ASP.Net Web API 的參數綁定[翻譯]

來源:互聯網
上載者:User

標籤:

原文地址:Parameter Binding in ASP.NET Web API

譯文如下:

     當Web API相應Controller的一個方法時,它必定存在一個設定參數的過程,叫作資料繫結。這篇文章描述了Web API如何綁定參數以及如何自訂綁定過程。

    一般情況下,Web API綁定參數符合如下規則:

    • 如果參數為簡單類型,Web API 嘗試從URI中擷取。簡單參數類型包含.Net源生類型(int,bool,double...),加上TimeSpan,DateTime,Guid,decimal和string.加上任何包含string轉化器的類型。(More about type converters later.)
    • 對複雜類型來說,Web API 試圖從message body 中讀取,使用media-type 類型。

  典型Web API Controller方法的例子:

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

  參數id是簡單類型,所以webapi試圖從URI中擷取值。參數item為複雜類型,所以web api 使用 media-type 類型從request body中讀取。

  從URI中擷取值,Web API會從路由或者URI的查詢參數中擷取。路由資料例子如下:"api/{controller}/public/{category}/{id}",更多細節可參考:Routing and Action Selection.

  在餘下的文章中,我將給你介紹的是如何自訂參數綁定的過程。對於複雜類型,當然最好儘可能考慮media-type類型轉化。一個http的關鍵原理是:資源是儲存在message body 中被傳遞的,使用內容協商來指定資源如何表示。media-type 類型就是被設計來實現這個目標的。

使用[FromUri]

  在參數前添加[FromUri]屬性可以強制Web API 從URI中讀取複雜類型。下面的例子定義了GeoPoint類型,以及一個從URI中擷取GeoPoint的controller方法。

public class GeoPoint{    public double Latitude { get; set; }     public double Longitude { get; set; }}public ValuesController : ApiController{    public HttpResponseMessage Get([FromUri] GeoPoint location) { ... }}

  用戶端可以通過查詢字元將兩個參數傳遞給Web API。例子如下:

http://localhost/api/values/?Latitude=47.678558&Longitude=-122.130989
使用【FromBody】

  給參數添加[FromBody]屬性可以迫使Web API從request body 中讀取簡單參數。

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

  在這個例子中WebAPI將使用一個 media-type 轉化器從Request body 中讀取name的值。

POST http://localhost:5076/api/values HTTP/1.1User-Agent: FiddlerHost: localhost:5076Content-Type: application/jsonContent-Length: 7"Alice"

  當一個參數標記[FromBody]後Web API通過Content-Type header選擇格式。在這個例子中,content type 是“application/json” 並且request body是原始的Json字串(不是Json對象)。

  有而且只有一個參數允許從message body 中讀取,下面的例子將不會成功:

// Caution: Will not work!    public HttpResponseMessage Post([FromBody] int id, [FromBody] string name) { ... }

  這條規則的原因為:request body 可能儲存在一個只能讀取一次的非緩衝的資料流中。

Type Converters

  建立一個TypeConverter並提供一個字串轉化,你就可以使Web API像對待一個簡單類型那樣對待一個類(所以Web API會嘗試從URI中擷取繫結資料)。

    下面的代碼展示了一個GeoPoint類代表地理座標,添加TypeConvert方法將字串轉化為GeoPoint執行個體。GeoPoint類添加了[TypeConvert]屬性,制定類型轉化器。(這個例子取自Mike Stall的部落格文章How to bind to custom objects in action signatures in MVC/WebAPI)

[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;    }}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視為簡單類型,也就是說他會試圖從URL中擷取GeoPoint參數,而不用在參數前添加[FromUri]

public HttpResponseMessage Get(GeoPoint location) { ... }

  用戶端請求的URI就像這樣:

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

  操控類型轉化更強的是建立自訂對象綁定Model Binder。使用Model Binder,你可以接收一個Http請求,一個Action和一個路由資料的原始值。

  建立一個Model Binder,需要繼承IModelBinder介面,這個介面只定義了一個方法BindModel

bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);

  下面是GeoPoint對象的Model Binder

public class GeoPointModelBinder : IModelBinder{    // List of known locations.    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(HttpActionContext actionContext, 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。

  WebAPI預設的Value Provider通過路由資料和URI的查詢參數擷取資料。如果URI是http://localhost/api/values/1?location=48,-122,Value Provider建立一個索引值對:

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

  (我假設預設的路由模板是"api/{controller}/{id}")

  綁定參數的名稱儲存在ModelBindingContext.ModelName屬性中,Model Binder在字典中尋找Key和它的值。如果值存在而且可以被轉化成GeoPoint,Model Binder將綁定的值分配給ModelBindingContext.Model屬性。

  注意:Model Binder並不只限定於簡單類型。在這個例子中,Model Binder首先尋找location類型的列表,如果失敗了才使用type Conversion。

設定 Model Binder

  有多種路徑可以設定Model Binder。首先,你可以給參數添加[ModelBinder]屬性。

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

  你也可以在類型上添加[ModelBinder]屬性。Web API會對所有此類型的參數指派Model Binder。

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

  最後,你也可以在HttpConfiguration中添加Model-binder Provider。model-binder provider是一個建立Model binder的簡單的工廠類。你可以通過繼承 ModelBinderProvider類來建立一個Provider.然而,如果你的model binder只處理一個類型,簡單的方法是嵌入SimpleModelBinderProvider,這也是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]屬性,去告訴WebAPI它應該使用model binder並且不是一個media-type類型。但是現在你不用在屬性中指定model binder的類型:

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

  

Value Providers

  我提到了model binder從Value provider中擷取值。通過實現IValueProvider介面建立自訂value provider。下面的列子展示了如何從一個Request請求的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;    }}

  你還必須建立一個繼承於ValueProviderFactory類的Value Provider工廠。

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,當我們調用ValueProvider.GetValue方法時,model binder接收到的是第一個可以產生它的Provider。

  另外一種方法是,你可以使用ValueProvider屬性,在參數級下設定ValueProvider 工廠,就像下面所示:

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

  這就會告訴Web API使用指定的value provider factory工廠,而不是其他註冊的value provider。

HttpParameterBinding

  模型繫結是一個普通機制中更常用的一個具體執行個體。如果你查看[ModelBinder]屬性,你就會發現他繼承於一個靜態類ParameterBindingAttribute。這個靜態類只定義了一個方法:GetBinding。這個方法返回一個HttpParameterBinding對象。

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

  HttpParameterBinding 負責將綁定參數轉化為值。如果設定了 [ModelBinder],屬性會返回一個實現了IModelBinder 介面的HttpParameterBinding 去實現實際的綁定。你也可以實現你自己的HttpParameterBinding

  舉例來說:假設你想從一個請求的headers中的if-match和if-none-match中擷取ETags.首先我們需要定義ETag類。

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

  再次我們需要定義一個枚舉來確定是從header中的if-match還是if-none-match中取得ETag。

public enum ETagMatch{    IfMatch,    IfNoneMatch}

  下面是一個從我們需要的header中擷取ETag並且把它綁定到ETag型別參數的HttpParameterBinding類的例子:

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 方法執行綁定。通過這個方法可以將已綁定的參數值添加到HttpActionContext中的ActionArgument 字典中。

  NOTE:如果你的ExecuteBindingAsync方法從請求串連中的Body讀取值。需要重寫WillReadBody 屬性並讓他返回true。因為請求的Body可能是只能讀一次的無緩衝資料流。所以Web API定義了強制執行的規則:只有一個綁定可以讀取請求的body。

  為了實現一個自訂HttpParameterBinding。你可以定義一個繼承自ParameterBindingAttribute的屬性。在ETagParameterBinding中定義了兩個屬性,一個是為if-match headers 另一個是為if-not-match headers。兩個都繼承自靜態基類。

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)    {    }}

  下例說明了在controller的方法中如何使用[IfNoneMatch]屬性:

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

  除了ParameterBindingAttribute,這裡還有另外一種關聯自訂HttpParameterBinding的方法。在HttpConfiguration 對象中,ParameterBindingRules 屬性是一個(HttpParameterDescriptor -> HttpParameterBinding)類型的匿名函數的集合。舉例來說,你可以添加一個規則,所有的包含ETag參數的Get方法使用if-none-match的ETagParameterBinding:

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

  整個參數綁定的過程被一個名為IActionValueBinder的可插拔的服務控制。IActionValueBinder的預設實現方式遵循以下步驟:

    1. 在參數中尋找ParameterBindingAttribute 。包含[FromBody][FromUri], 和[ModelBinder]或自訂的屬性。
    2. 否則,從HttpConfiguration.ParameterBindingRules中尋找一個返回非空的HttpParameterBinding的函數。
    3. 否則,使用上面所描述的預設規則。
      • 如果參數簡單或者有類型轉化器,綁定在URI上。這就相當於給參數添加了 [FromUri]屬性。
      • 否則,嘗試從訊息體中讀取參數。相當於給參數添加了[FromBody] 屬性。

  如果你想,你可以使用自訂的實現代替整個IActionValueBinder 服務。

相關資源:

  Custom Parameter Binding Sample

  Mike Stall謝了非常好的一系列的關於Web API 參數綁定的blog文章:

    • How Web API does Parameter Binding
    • MVC Style parameter binding for Web API
    • How to bind to custom objects in action signatures in MVC/Web API
    • How to create a custom value provider in Web API
    • Web API Parameter binding under the hood

ASP.Net Web API 的參數綁定[翻譯]

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.