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