当 WebAPI 调用 Controller 上的方法时, 必须为其参数赋值, 这个过程就是参数绑定。 本文介绍 WebAPI 如何绑定参数, 以及如何进行自定义。
WebAPI 默认使用下面的规则进行参数绑定:
int
、 bool
、 float
、 double
等);TimeSpan
、 DateTime
、 Guid
、 decimal
和 string
;media-type formatter
从 HTTP 请求的正文 (body) 中读取。比如一个典型的 WebAPI 方法:
IHttpActionResult Put(int id, Product item) { ... }
参数 id
是一个简单类型, 所以从 request URI 中取值, 而参数 item
是复杂类型, 则从 request 正文 (body) 中取值。
要强制 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
服务端就可以接收到数组参数了。
要强制 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.1
User-Agent: Fiddler
Host: localhost:5076
Content-Type: application/json
Content-Length: 7
"Alice"
当一个参数有 [FromBody]
标记时, WebAPI 使用 Content-Type
标头来选择正确的格式, 在上面的例子中, Content-Type
是 application/json
, request正文 (body) 的内容是原始的 JSON 字符串, 而不是一个 JSON 对象。
> 一个函数中, 最多只能有一个 [FromBody]
标记, 因为客户端的请求有可能没有缓冲, 只能被读取一次。
通过创建 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
另一个比 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
;首先, 可以在 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
) { ... }
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 。
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
的可插拔的服务控制,默认的按照下面的规则进行参数绑定:
, 包括
[FromBody] 、
[FromUri] 、
[ModelBinder]` 或者其它自定义标记;HttpConfiguration.ParameterBindingRules
中查找一个返回 HttpParameterBinding
实例的函数;[FromUri]
标记;[FromBody]
标记。如果默认的绑定不能满足需求, 也可以实现自定义的 IActionValueBinder
来替换掉 Web API 默认的实现。