導航 閱讀本文之前,您也可以到Asp.Net Web API 2 系列導航進行查看 http://www.cnblogs.com/aehyok/p/3446289.html。 本文主要來講解以下內容: 〇、前言 Ⅰ、Using[FromUri] Ⅱ、Using[FromBody] Ⅲ、Type Co ...
導航
閱讀本文之前,您也可以到Asp.Net Web API 2 系列導航進行查看 http://www.cnblogs.com/aehyok/p/3446289.html。
本文主要來講解以下內容:
閱讀本文之前,您也可以到Asp.Net Web API 2 系列導航進行查看 http://www.cnblogs.com/aehyok/p/3446289.html。
當Web API在一個控制器中調用一個方法的時候,它必須為參數設定值,這個過程就叫做綁定。這篇文章描述Web API如何綁定參數,以及如何自定義綁定過程。
預設情況,Web API使用如下規則來綁定參數:
1、如果參數一個"簡單"類型,那麼Web API試圖從URI中獲取值。簡單的類型包含.NET的基元類型(int,bool,double等等)加上TimeSpan, DateTime, Guid, decimal, and string,再加上任何的能從字元串進行轉換的類型。
2、對於複雜類型,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來指定資源的展現。媒體格式化器被設計就是為了這個目的。
為了更好的讓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
為了更好的讓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) { ... }
對於這個規則的原因就是這個請求體被存儲在只能被讀取一次的非緩衝流中。
你也可以讓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
比一個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中獲得原始的錄入值。這個設計分為兩個獨立的方法:
1、這個value provider接收到一個HTTP請求,並且填充一個鍵值對的字典。
2、然後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) { ... }
我前面提到過一個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。
模型綁定是一個更加普遍機制的特性實例。如果你看到這個 [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。IActionValueBinder 的預設實現將執行以下操作:
1、在參數上查看ParameterBindingAttribute ,這包括 [FromBody], [FromUri], 和[ModelBinder], 或者是自定義的屬性。
2、否則,查看一個函數的HttpConfiguration.ParameterBindingRules ,它返回一個非null的HttpParameterBinding 。
3、否則,使用我之前描述的預設規則。
①、如果參數類型是一個“簡單”的,或者擁有一個type converter,將會從URI進行綁定。它等價於在參數上添加[FromUri]屬性。
②、否則,試圖從消息體中讀取參數,這等價於在參數上添加[FromBody]屬性。
如果你需要,你可以用一個自定義的實現來替代整個IActionValueBinder 。
總結
本文主要來講解參數綁定,但是通過上面也可以看出涉及到的知識點還是蠻多的,但是都是很實用的,例子也比較清晰。但是還是需要在項目中進行應用,才能更好的學習和掌握參數綁定的環節。
本文的參考鏈接為http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api
參考頁面:
http://www.yuanjiaocheng.net/webapi/create-crud-api-1-delete.html
http://www.yuanjiaocheng.net/webapi/Consume-web-api.html
http://www.yuanjiaocheng.net/webapi/mvc-consume-webapi-get.html
http://www.yuanjiaocheng.net/webapi/mvc-consume-webapi-post.html
http://www.yuanjiaocheng.net/webapi/mvc-consume-webapi-put.html