当 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 的转换:
[TypeConverter(typeof(GeoPointConverter))]
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] 标记, 例如:
[ModelBinder(typeof(GeoPointModelBinder))]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跨平台或扫描二维码关注
