Asp.Net Web API 2第十六課——Parameter Binding in ASP.NET Web API(參數綁定)

導航html

閱讀本文以前,您也能夠到Asp.Net Web API 2 系列導航進行查看 http://www.cnblogs.com/aehyok/p/3446289.htmlgit

本文主要來說解如下內容:web

  〇、前言json

  Ⅰ、Using[FromUri]api

  Ⅱ、Using[FromBody]cookie

  Ⅲ、Type Convertersapp

  Ⅳ、Model Bindersasp.net

  Ⅴ、Value Providerside

  Ⅵ、HttpParameterBinding函數

  Ⅶ、IActionValueBinder

前言

閱讀本文以前,您也能夠到Asp.Net Web API 2 系列導航進行查看 http://www.cnblogs.com/aehyok/p/3446289.html

當Web API在一個控制器中調用一個方法的時候,它必須爲參數設定值,這個過程就叫作綁定。這篇文章描述Web API如何綁定參數,以及如何自定義綁定過程。

  默認狀況,Web API使用以下規則來綁定參數:

  一、若是參數一個"簡單"類型,那麼Web API試圖從URI中獲取值。簡單的類型包含.NET的基元類型(int,bool,double等等)加上TimeSpanDateTimeGuiddecimal, and string,再加上任何的能從字符串進行轉換的類型。

  二、對於複雜類型,Web API試圖用媒體格式化器http://www.cnblogs.com/aehyok/p/3460164.html從消息體中來讀取值。

例如,這是一個典型的Web API控制器方法:

HttpResponseMessage Put(int id, Product item) { ... }

這個「id」參數是一個「簡單」類型,所以Web API試圖從請求的URI中獲取參數值,這個「item」參數是一個複雜類型,所以Web API試圖使用一個媒體格式化器從請求消息體中來讀取參數值。

爲了從URI中獲取值,Web API會查看路由數據和URI查詢字符串。這個路由數據被填充是在路由系統解析URI並匹配它到路由的時候。對於路由的更多信息: http://www.cnblogs.com/aehyok/p/3444710.html

在這篇文章剩餘的部分我未來展現如何自定義模型綁定的過程。對於複雜類型,要儘量的使用媒體格式化器來處理。HTTP的一個主要原則就是資源被髮送在消息體中,使用內容協商http://www.cnblogs.com/aehyok/p/3481265.html來指定資源的展示。媒體格式化器被設計就是爲了這個目的。

Using [FromUri]

 爲了更好的讓Web API從URI中讀取複雜類型,添加【FormUri】屬性到參數上。下面的例子定義了一個GeoPoint 的類型,緊接着一個控制器方法從URI中得到這個GetPoint參數。

public class GeoPoint
{
    public double Latitude { get; set; } 
    public double Longitude { get; set; }
}

public ValuesController : ApiController
{
    public HttpResponseMessage Get([FromUri] GeoPoint location) { ... }
}

這個客戶端能夠把Latitude和Longitude的值放進查詢字符串中。Web API將用這兩個參數來構造一個GeoPoint參數。例如:

http://localhost/api/values/?Latitude=47.678558&Longitude=-122.130989

Using [FromBody]

 爲了更好的讓Web API 從消息體中讀取一個簡單類型。添加【FromBody】屬性到參數上:

public HttpResponseMessage Post([FromBody] string name) { ... }

在這個例子中,Web API將使用媒體格式化器來讀取消息體中的name值。這是一個客戶端請求的例子:

POST http://localhost:5076/api/values HTTP/1.1
User-Agent: Fiddler
Host: localhost:5076
Content-Type: application/json
Content-Length: 7

"Alice"

當一個參數擁有【FromBody】屬性的時候,Web API使用Content-Type header去選擇一個格式化器。在這個例子中Content-Type是「application/json」,這個請求體是一個原始的Json字符串(而不是Json對象)。

至多一個參數被容許從消息體中讀取值。所以以下這段將不會起做用:

public HttpResponseMessage Post([FromBody] int id, [FromBody] string name) { ... }

對於這個規則的緣由就是這個請求體被存儲在只能被讀取一次的非緩衝流中。

Type Converters

 你也可讓Web API對待一個class像一個簡單的類型,經過建立一個TypeConverter 並提供一個字符串的轉換。

接下來的代碼展現了用一個GeoPoint類來表示一個地理位置。添加一個 TypeConverter來把字符串轉換爲GeoPoint實例。這個GeoPoint類用了一個TypeConverter屬性來修飾,而且指定了這個TypeConverter的類型。

    [TypeConverter(typeof(GeoPointConverter))]
    public class GeoPoint
    {
        public double Latitude { get; set; }
        public double Longitude { get; set; }
        public static bool TryParse(string s, out GeoPoint result)
        {
            result = null;

            var parts = s.Split(',');
            if (parts.Length != 2)
            {
                return false;
            }

            double latitude, longitude;
            if (double.TryParse(parts[0], out latitude) &&
                double.TryParse(parts[1], out longitude))
            {
                result = new GeoPoint() { Longitude = longitude, Latitude = latitude };
                return true;
            }
            return false;
        }
    }
    public class GeoPointConverter:TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            if (sourceType == typeof(string))
            {
                return true;
            }
            return base.CanConvertFrom(context, sourceType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context,
            CultureInfo culture, object value)
        {
            if (value is string)
            {
                GeoPoint point;
                if (GeoPoint.TryParse((string)value, out point))
                {
                    return point;
                }
            }
            return base.ConvertFrom(context, culture, value);
        }
    }

如今Web API能夠把GeoPoint看作是一個簡單類型。意味着它將能夠從URI中綁定GeoPoint參數。在參數上你不須要添加【FromUri】屬性。

客戶端能夠調用這個方法,例如以下的URI:

http://localhost/api/values/?location=47.678558,-122.130989

Model Binders

 比一個type converter更靈活的選項是建立一個自定義的模型綁定。有了模型綁定,你可使用像HTTP請求,Action描述,以及路由數據中的原始值。

爲了建立一個Model Binder,你須要實現IModelBinder 接口,這個接口中定義了一個方法,BindModel:

bool BindModel(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext);

接下來爲GeoPoint對象來建立一個Model Binder。

    public class GeoPointModelBinder:IModelBinder
    {
        private static ConcurrentDictionary<string, GeoPoint> _locations
    = new ConcurrentDictionary<string, GeoPoint>(StringComparer.OrdinalIgnoreCase);

        static GeoPointModelBinder()
        {
            _locations["redmond"] = new GeoPoint() { Latitude = 47.67856, Longitude = -122.131 };
            _locations["paris"] = new GeoPoint() { Latitude = 48.856930, Longitude = 2.3412 };
            _locations["tokyo"] = new GeoPoint() { Latitude = 35.683208, Longitude = 139.80894 };
        }
        public bool BindModel(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.ModelType != typeof(GeoPoint))
            {
                return false;
            }

            ValueProviderResult val = bindingContext.ValueProvider.GetValue(
                bindingContext.ModelName);
            if (val == null)
            {
                return false;
            }

            string key = val.RawValue as string;
            if (key == null)
            {
                bindingContext.ModelState.AddModelError(
                    bindingContext.ModelName, "Wrong value type");
                return false;
            }

            GeoPoint result;
            if (_locations.TryGetValue(key, out result) || GeoPoint.TryParse(key, out result))
            {
                bindingContext.Model = result;
                return true;
            }

            bindingContext.ModelState.AddModelError(
                bindingContext.ModelName, "Cannot convert value to Location");
            return false;
        }
    }

一個model binder從一個value provider中得到原始的錄入值。這個設計分爲兩個獨立的方法:

一、這個value provider接收到一個HTTP請求,而且填充一個鍵值對的字典。

二、而後model binder使用鍵值對的字典來填充model。

Web API中默認的value provider從路由數據和查詢字符串中獲取值。例如,這樣一個URI:

http://localhost/api/values/1?location=48,-122

value provider將會建立以下的鍵值對:

id = "1"
location = "48,122"

咱們假設使用的是默認的路由模版。

被綁定的參數的名稱被存儲在ModelBindingContext.ModelName這個屬性上。model binder在字典中尋找一個鍵的值。若是這個值存在,而且也能被轉換成GeoPoint,這個model binder將分配這個值到ModelBindingContext.Model屬性。

注意:Model Binder不會限制一個簡單類型的轉換,這個model binder首先會在已知位置的列表中尋找,若是查找失敗,將會使用 type converter。

Setting the Model Binder

這裏有幾種方法去設置Model Binder.首先你能夠添加一個【Model Binder】屬性到參數上。

public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)

你也能添加【Model Binder】屬性到這個參數類型上。Web API將指定這個model binder到這個類型的全部參數上。

[ModelBinder(typeof(GeoPointModelBinder))]
public class GeoPoint
{
    // ....
}

最後,你能添加一個model-binder的提供者到HttpConfiguration。一個model-binder的提供者就是一個簡單的工廠類,它能夠建立一個model binder。你能建立一個provider經過派生自 ModelBinderProvider類。不管怎樣,若是你的model binder處理單個類型,它是比較容易的經過使用已經建立的SimpleModelBinderProvider

 

接下來的代碼展現如何啓用他們:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var provider = new SimpleModelBinderProvider(
            typeof(GeoPoint), new GeoPointModelBinder());
        config.Services.Insert(typeof(ModelBinderProvider), 0, provider);

        // ...
    }
}

有了一個model-binding provider,你仍然須要添加一個[ModelBinder] 屬性到參數上,它的目的就是告知Web API應該是用model binder,而不是使用媒體格式化器。可是如今你不須要在屬性上指定這個model binder的類型。

public HttpResponseMessage Get([ModelBinder] GeoPoint location) { ... }

Value Providers

 我前面提到過一個model binder是從value provider中獲取值。寫一個自定義的value provider,實現這個IValueProvider 接口。這個例子是從請求的cookie中獲取值。

    public class CookieValueProvider:IValueProvider
    {
        private Dictionary<string, string> _values;

        public CookieValueProvider(HttpActionContext actionContext)
        {
            if (actionContext == null)
            {
                throw new ArgumentNullException("actionContext");
            }

            _values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            foreach (var cookie in actionContext.Request.Headers.GetCookies())
            {
                foreach (CookieState state in cookie.Cookies)
                {
                    _values[state.Name] = state.Value;
                }
            }
        }

        public bool ContainsPrefix(string prefix)
        {
            return _values.Keys.Contains(prefix);
        }

        public ValueProviderResult GetValue(string key)
        {
            string value;
            if (_values.TryGetValue(key, out value))
            {
                return new ValueProviderResult(value, value, CultureInfo.InvariantCulture);
            }
            return null;
        }
    }

你也須要建立一個value provider 工廠經過繼承自ValueProviderFactory 。

    public class CookieValueProviderFactory : ValueProviderFactory
    {
        public override IValueProvider GetValueProvider(HttpActionContext actionContext)
        {
            return new CookieValueProvider(actionContext);
        }
    }

添加value provider 工廠到HttpConfiguration ,代碼以下:

public static void Register(HttpConfiguration config)
{
    config.Services.Add(typeof(ValueProviderFactory), new CookieValueProviderFactory());

    // ...
}

 Web API組合了全部的value provider,所以當一個model binder調用ValueProvider.GetValue,這個model binder接收從第一個value provider能提供它的值。

或者,經過使用ValueProvider屬性你也能在參數級別上設置value provider 工廠,代碼以下:

public HttpResponseMessage Get(
    [ValueProvider(typeof(CookieValueProviderFactory))] GeoPoint location)

這將告訴Web API模型綁定使用指定的value  provider 工廠,不要用任何另外的被註冊的value  provider。

HttpParameterBinding

 模型綁定是一個更加廣泛機制的特性實例。若是你看到這個 [ModelBinder] 屬性,你將明白它是派生自ParameterBindingAttribute 抽象類。這個類定義了一個單獨的方法,並返回一個HttpParameterBinding 對象:

public abstract class ParameterBindingAttribute : Attribute
{
    public abstract HttpParameterBinding GetBinding(HttpParameterDescriptor parameter);
}

HttpParameterBinding 對象負責綁定一個值到一個參數。在[ModelBinder]修飾的狀況下,這個屬性返回一個HttpParameterBinding 的實現,它使用了一個IModelBinder 去展示真實的binding。你也能夠實現本身的HttpParameterBinding

 例如,假定你想從請求的if-match 和 if-none-match 的header中獲取ETags。開始咱們將定義一個class來代替ETags 。

public class ETag
{
    public string Tag { get; set; }
}

咱們也來定義一個枚舉指明是否從if-match 和 if-none-match 的header中得到了ETag 。

public enum ETagMatch
{
    IfMatch,
    IfNoneMatch
}

這裏是HttpParameterBinding ,獲取該 ETag 從所需的標頭並將其綁定到類型的參數的 ETag:

public class ETagParameterBinding : HttpParameterBinding
    {
        ETagMatch _match;

        public ETagParameterBinding(HttpParameterDescriptor parameter, ETagMatch match)
            : base(parameter)
        {
            _match = match;
        }

        public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
            HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            EntityTagHeaderValue etagHeader = null;
            switch (_match)
            {
                case ETagMatch.IfNoneMatch:
                    etagHeader = actionContext.Request.Headers.IfNoneMatch.FirstOrDefault();
                    break;

                case ETagMatch.IfMatch:
                    etagHeader = actionContext.Request.Headers.IfMatch.FirstOrDefault();
                    break;
            }

            ETag etag = null;
            if (etagHeader != null)
            {
                etag = new ETag { Tag = etagHeader.Tag };
            }
            actionContext.ActionArguments[Descriptor.ParameterName] = etag;

            var tsc = new TaskCompletionSource<object>();
            tsc.SetResult(null);
            return tsc.Task;
        }
    }

ExecuteBindingAsync 方法來處理綁定。在此方法中,添加參數值到ActionArgument 字典中並在HttpActionContext中。

若是你的ExecuteBindingAsync 方法讀取請求消息體。重寫這個WillReadBody 屬性去返回true。這個消息體多是隻能讀一次的未緩衝的流。所以Web API施行了一個規則至多有一個綁定能夠讀取消息體。

應用一個自定義的HttpParameterBinding,你能定義一個派生自ParameterBindingAttribute 的屬性,對於ETagParameterBinding,咱們將定義兩個屬性:一個是對於if-match  Header的,一個是對於if-none-match Header。都派生自一個抽象的基類。

public abstract class ETagMatchAttribute : ParameterBindingAttribute
{
    private ETagMatch _match;

    public ETagMatchAttribute(ETagMatch match)
    {
        _match = match;
    }

    public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
    {
        if (parameter.ParameterType == typeof(ETag))
        {
            return new ETagParameterBinding(parameter, _match);
        }
        return parameter.BindAsError("Wrong parameter type");
    }
}

public class IfMatchAttribute : ETagMatchAttribute
{
    public IfMatchAttribute()
        : base(ETagMatch.IfMatch)
    {
    }
}

public class IfNoneMatchAttribute : ETagMatchAttribute
{
    public IfNoneMatchAttribute()
        : base(ETagMatch.IfNoneMatch)
    {
    }
}

這是一個控制器方法 使用了[IfNoneMatch] 屬性。

public HttpResponseMessage Get([IfNoneMatch] ETag etag) { ... }

除了ParameterBindingAttribute 以外,對於添加一個自定義的HttpParameterBinding 有另一個掛鉤。在HttpConfiguration 對象上,ParameterBindingRules 是一個匿名方法類型(HttpParameterDescriptor -> HttpParameterBinding)的集合。例如,你能夠添加一個規則:在Get請求方法中任何ETag 參數使用ETagParameterBinding with if-none-match。

config.ParameterBindingRules.Add(p =>
{
    if (p.ParameterType == typeof(ETag) && 
        p.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Get))
    {
        return new ETagParameterBinding(p, ETagMatch.IfNoneMatch);
    }
    else
    {
        return null;
    }
});

這個方法對於綁定不適用的參數應該返回null。

IActionValueBinder

 整個參數綁定的過程被一個可插拔的服務控制,IActionValueBinderIActionValueBinder 的默認實現將執行如下操做:

  一、在參數上查看ParameterBindingAttribute ,這包括 [FromBody][FromUri], 和[ModelBinder], 或者是自定義的屬性。

  二、不然,查看一個函數的HttpConfiguration.ParameterBindingRules ,它返回一個非null的HttpParameterBinding

  三、不然,使用我以前描述的默認規則。

    ①、若是參數類型是一個「簡單」的,或者擁有一個type converter,將會從URI進行綁定。它等價於在參數上添加[FromUri]屬性。

    ②、不然,試圖從消息體中讀取參數,這等價於在參數上添加[FromBody]屬性。

 若是你須要,你能夠用一個自定義的實現來替代整個IActionValueBinder 。

總結

本文主要來說解參數綁定,可是經過上面也能夠看出涉及到的知識點仍是蠻多的,可是都是很實用的,例子也比較清晰。可是仍是須要在項目中進行應用,才能更好的學習和掌握參數綁定的環節。

本文的參考連接爲http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api 

相關文章
相關標籤/搜索