ASP.NET WebAPI 中的参数绑定
當 WebAPI 調用 Controller 上的方法時, 必須為其參數賦值, 這個過程就是參數綁定。 本文介紹 WebAPI 如何綁定參數, 以及如何進行自定義。
WebAPI 默認使用下面的規則進行參數綁定:
簡單類型, WebAPI 嘗試從 URL 中獲取它的值。 簡單類型包括:
.NET?原始類型(int、?bool、?float、?double?等);
以及?TimeSpan?、?DateTime?、?Guid、?decimal?和?string;
提供了類型轉換器 (Type Converter), 能夠從字符串轉換的類型。
復雜類型則使用?media-type formatter?從 HTTP 請求的正文 (body) 中讀取。
比如一個典型的 WebAPI 方法:
IHttpActionResult Put(int id, Product item) { ... }參數?id?是一個簡單類型, 所以從 request URI 中取值, 而參數?item?是復雜類型, 則從 request 正文 (body) 中取值。
使用 [FromUri]
要強制 WebAPI 從 URL 讀取一個復雜類型的參數, 則需要在該參數上添加?FromUri?標記。 下面的例子定義了一個?GeoPoint?類型, 以及如何從 URI 中獲取?GeoPoint?實例。
public class GeoPoint {public double Latitude { get; set; }public double Longitude { get; set; }}public class TestController : ApiController {public IHttpActionResult Get([FromUri]GeoPoint location) { ... }}客戶端可以在 QueryString 中傳遞 Latitude 和 Longitude 來構造 GeoPoint 實例, 示例請求如下:
http://127.0.0.1/api/test?latitude=22.3&longitude=113.2注: QueryString 中的參數名稱是不區分大小寫的。
對于數組類型, 也可以使用?[FromUri]?標記, 比如:
public IHttpActionResult Get([FromUri]int[] items) { ... }客戶端這樣發送請求:
http://127.0.0.1/api/test?items=1&items=2&items=3服務端就可以接收到數組參數了。
使用 [FromBody]
要強制 WebAPI 從 request正文 (body) 中讀取一個簡單類型的參數, 需要在該參數上添加?FromBody?標記:
public HttpResponseMessage Post([FromBody] string name) { ... }在這個例子中, WebAPI 需要使用?media-type formatter?從 request正文 (body) 中讀取?name?的值, 示例請求如下:
POST http://localhost:5076/api/values HTTP/1.1User-Agent: FiddlerHost: localhost:5076Content-Type: application/json Content-Length: 7"Alice"當一個參數有?[FromBody]?標記時, WebAPI 使用?Content-Type?標頭來選擇正確的格式, 在上面的例子中, Content-Type?是?application/json?, request正文 (body) 的內容是原始的 JSON 字符串, 而不是一個 JSON 對象。
> 一個函數中, 最多只能有一個?[FromBody]?標記, 因為客戶端的請求有可能沒有緩沖, 只能被讀取一次。
使用 Type Converter
通過創建?Type Converter?, 實現從字符串轉換的方法, 可以讓 WebAPI 將復雜類型參數視為簡單類型參數。
以上面的?GeoPoint?為例, 再提供一個?GeoPointConverter?實現從字符串到?GeoPoint?的轉換:
[]public class GeoPoint {public double Latitude { get; set; }public double Longitude { get; set; }public 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);}}
現在, WebAPI 會將?GeoPoint?當作簡單類型, 意味著將嘗試從 URI 中綁定 GeoPoint 參數的值, 也不再需要?[FromUri]?標記:
public HttpResponseMessage Get(GeoPoint location) { ... }客戶端這樣發送 HTTP 請求:
https://127.0.0.1/api/test?location=22.3,113.2使用 Model Binder
另一個比?type converter?更加靈活的是創建自定義?Model Binder?。 通過?Model Binder?, 可以直接訪問 http 請求、 action 描述以及路由的原始值。
要創建?Model Binder?, 需要實現接口?IModelBinder?, 它只定義了一個方法?BindModel?:
public interface IModelBinder {bool BindModel( ? ? ? ?HttpActionContext actionContext, ? ? ? ?ModelBindingContext bindingContext ? ?);}下面是針對?GeoPoint?的實現:
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;}// exit if no value from value provider ? ? ? ?var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);if (val == null) {return false;}// exit if row value is not a string. ? ? ? ?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 不止局限于簡單類型, 也支持復雜類型。 上面的 MobelBinder 支持兩種格式的查詢:
使用已知的地名:?http://127.0.0.1:/rest/api/test?location=redmond?;
使用經緯度:?http://127.0.0.1:/rest/api/test?location=47.67856,-122.131?;
設置 Model Binder
首先, 可以在 action 方法的參數上添加?[ModelBinder]?標記, 例如:
public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)其次, 可以在?GeoPoint?類型上添加 [ModelBinder] 標記, 例如:
[]public class GeoPoint {// ....}最后, 還可以在?HttpConfiguration?類中添加一個?model-binder provider?來使用, 代碼如下:
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);// ... ? ?}}在 action 方法中仍然需要為參數添加?[ModelBinder]?標記, 來說明該參數需要使用?model-binder?來而不是?media formatter?來進行參數綁定, 不過此時就不需要再指定 ModelBinder 的類型了:
public HttpResponseMessage Get( ? ?[ModelBinder] GeoPoint location) { ... }使用 ValueProvider
Model Binder?需要從?Value Provider?中取值, 因此也可以創建自定義的?Value Provider?實現獲取特殊的值。 要實現自定義的?ValueProvider?, 需要實現接口?IValueProvider?, 下面是一個從 Cookie 中獲取值的?CookieValueProvider?:
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?的?CookieValueProviderFactory?, 代碼如下:
public class CookieValueProviderFactory : ValueProviderFactory {public override IValueProvider GetValueProvider(HttpActionContext actionContext) {return new CookieValueProvider(actionContext);}}然后將?CookieValueProviderFactory?注冊到?HttpConfiguration?實例:
public static void Register(HttpConfiguration config) {config.Services.Add(typeof(ValueProviderFactory),new CookieValueProviderFactory());// ...}Web API 將組合所有的?ValueProviderFactory?, 當一個 model binder 調用?ValueProvider.GetValue?方法時, 將會收到第一個能夠提供對應值的?ValueProviderFactory?提供的值。
或者, 也可以直接在在參數上使用?ValueProviderAttribute?標記:
public HttpResponseMessage Get( ? ?[ValueProvider(typeof(CookieValueProviderFactory))] GeoPoint location) { ... }這樣, Web API 在處理這個參數時, 就會直接使用?CookieValueProviderFactory?, 不再使用其它的 CookieValueProviderFactory 。
HttpParameterBinding
Model binder?只是參數綁定中的一個特定的實例, 如果查看?ModelBinderAttribute?類的定義, 會發現它繼承自抽象類?ParameterBindingAttribute?, 這個類只定義了一個方法?GetBinding?, 返回一個?HttpParameterBinding?實例。
public abstract class ParameterBindingAttribute : Attribute {public abstract HttpParameterBinding GetBinding(HttpParameterDescriptor parameter);}HttpParameterBinding?負責將參數綁定到值, 以?[ModelBinder]?為例, 這個標記返回一個?HttpParameterBinding?實現, 使用?IModelBinder?進行具體的綁定。 當然, 也可以實現自定義的?HttpParameterBinding?。
假設要獲取 HTTP 請求 Header 中的?if-match?和?if-none-match?標簽 (ETag) , 先定義一個類來表示 ETag :
public class ETag {public string Tag { get; set; }}同時再定義一個枚舉來指定是從?if-match?還是?if-none-match?標頭中獲取 ETag:
public enum ETagMatch {IfMatch,IfNoneMatch}接下來是從 HTTP 請求頭中獲取?ETag?的?ETagParameterBinding?,
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?字典中。
注意, 如果自定義的?HttpParameterBinding?需要從 HTTP 請求的正文 (body) 中讀取信息, 則需要重寫?WillReadBody?并返回?true?。 由于 HTTP 請求正文可能是個沒有緩沖的流, 只能讀取一次, 所以 Web API 加強了一個規則, 那就是每個方法只有一個綁定能夠從 HTTP 請求正文讀取數據。
要使用自定義的?HttpParameterBinding?, 則需要創建一個自定義的標記, 繼承自?ParameterBindingAttribute。 針對上面的?ETagParameterBinding?, 我們來定義兩個自定義標記, 分別表示從?if-match?和?if-none-match標頭中獲取, 代碼如下:
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) { ... }除了直接使用這個標記, 也可以在?HttpConfiguration?中進行配置, 代碼如下:
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?的可插拔的服務控制,默認的按照下面的規則進行參數綁定:
在參數上查找 ` ParameterBindingAttribute?, 包括?[FromBody]?、?[FromUri]?、?[ModelBinder]` 或者其它自定義標記;
然后在?HttpConfiguration.ParameterBindingRules?中查找一個返回?HttpParameterBinding?實例的函數;
最后, 使用上面提到的默認規則:
如果參數是一個簡單類型或者指定了類型轉換器, 從 URI 綁定, 相當于在參數上添加?[FromUri]?標記;
否則, 嘗試從 HTTP 請求正文中讀取, 相當于在參數上添加?[FromBody]?標記。
如果默認的綁定不能滿足需求, 也可以實現自定義的?IActionValueBinder?來替換掉 Web API 默認的實現。
原文地址:http://beginor.github.io/2017/06/25/parameter-binding-in-aspnet-web-api.html
.NET社區新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關注
總結
以上是生活随笔為你收集整理的ASP.NET WebAPI 中的参数绑定的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: asp.net core 1.1 项目升
- 下一篇: ASP.NET Core 源码学习之 O