ASP.NET Web API编程——模型验证与绑定
1.模型验证
使用特性约束模型属性
可以使用System.ComponentModel.DataAnnotations提供的特性来限制模型。
例如,Required特性表示字段值不能为空,Range特性限制数值类型的范围。
对实体类使用特性后,可以使用ModelState.IsValid来判断验证是否通过。
例:
实体:
public class DataModel { public int Id { get; set; } public string Field1Name {get;set;} [Required] public string Field2Name { get; set; } }
控制器操作:
[HttpPost] public IHttpActionResult ModelValid(DataModel model) { if (!ModelState.IsValid) { throw new HttpResponseException(HttpStatusCode.BadRequest); } return Ok(model); }
客户端调用:
HttpClient client = new HttpClient(); string url = "http://localhost/WebApi_Test/api/account/modelvalid"; using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url)) { var cont = new { Id = 1, Field1Name = "1name" }; HttpContent content = new StringContent(JsonConvert.SerializeObject(cont)); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); request.Content = content; HttpResponseMessage response = client.SendAsync(request).Result; Console.WriteLine("状态码:{0}",(int)response.StatusCode); var task = response.Content.ReadAsStringAsync(); task.Wait(); Console.WriteLine("结果:{0}", task.Result); }
输出结果:
服务端运行截图:
若客户端传值为:var cont = new { Id = 1, Field1Name = "1name", Field2Name="2name" };
默认赋值
Web API会对客户端未指定的模型属性赋初值。对于int,double等数值类型默认的初值为0,对于字符串或引用类型默认的初值是null。如果未对属性使用特性加以约束,那么ModelState.IsValid的值就是true,若对这样的属性应用Required特性,那么当客户端为对其赋初值时,验证将无法通过,即ModelState.IsValid的值为false。
例:
上例中不对Id属性赋值,运行客户端结果为:
可见框架自动为int型的Id赋初值0。
过载
此外当客户端所用实体属性多于服务端时,服务端会忽略多出来的属性,但建议控制器操作(Action)所用参数列表的参数或类属性与客户端所传参数完全匹配。
例:
若使用上述客户端,但传值为
var cont = new { Field1Name = "1name", Field2Name = "2name",FieldOverLoad ="overload"};
其中DataModel不包含FieldOverLoad 字段。
运行结果如下:
过滤验证结果
可以自定义操作过滤器来统一处理模型验证失败的情形。自定义操作过滤器派生自ActionFilterAttribute,我们需要重写OnActionExecuting方法,以便在操作(Action)调用之前处理。
例:
using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; using System.Web.Http.ModelBinding; namespace MyApi.Filters { public class ValidateModelAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { if (actionContext.ModelState.IsValid == false) { actionContext.Response = actionContext.Request.CreateErrorResponse( HttpStatusCode.BadRequest, actionContext.ModelState); } } } }
在WebApiConfig的Register方法中将上述自定义过滤器添加进来,这样过滤器对每一个操作(Action)都起作用,如果不想使其对每一个操作都起作用,而是想应用于个别操作(Action),可以将此特性应用到目标操作(Action)
public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.Filters.Add(new ValidateModelAttribute()); // ... } } public class ProductsController : ApiController { [ValidateModel] public HttpResponseMessage Post(Product product) { // ... } }
2模型绑定
默认的绑定规则
1)如果操作(Action)参数是简单类型,Web API框架会从URI中获取值。简单类型是指:.NET 框架定义的原始类型(int, bool, double等)、TimeSpan、DateTime、Guid、decimal、string;另外还有包含类型转换器的类型,改转换器可将字符串转换为此类型。这里从URI获取值具体指:从路由词典中获取值或者从URI的查询字符串中获取值。具体过程见介绍路由那篇博文。
2)对于复杂类型,Web API会使用多媒体格式化器从消息体中获得值。
类型转换
默认的模型绑定规则中提及了包含类型转换器的类型也是简单类型。类型转换器可以使类也被看做简单类型。这样按照默认的规则就可以从URI中获取值来构建参数列表了。
例:使用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; } } 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); } }
使用[FromUri]
为了强制Web API从URI中取值,可以使用FromUri特性。这样即使操作(Action)参数是复杂类型,框架也会中URI中取值来为参数赋值。
使用[FromBody]
为了强制Web API从消息体中取值,可以使用FromBody特性。这样即使操作(Action)参数是简单类型,框架也会从消息体中取值来为参数赋值。当使用FromBody特性时,Web API使用请求的Content-Type标头来选择格式化器。
注意:对多个参数使用FromBody不起作用。
例:
服务端操作为:
[HttpPost] public IHttpActionResult ModelValid([FromBody]DataModel model) { if (!ModelState.IsValid) { throw new HttpResponseException(HttpStatusCode.BadRequest); } return Ok(model); }
客户端调用为:
HttpClient client = new HttpClient(); string url = "http://localhost/WebApi_Test/api/account/modelvalid"; using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url)) { //var cont = new { Id = 1, Field1Name = "111" }; var cont = new { Field1Name = "1name", Field2Name = "2name"}; HttpContent content = new StringContent(JsonConvert.SerializeObject(cont)); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); request.Content = content; HttpResponseMessage response = client.SendAsync(request).Result; Console.WriteLine("状态码:{0}",(int)response.StatusCode); var task = response.Content.ReadAsStringAsync(); task.Wait(); Console.WriteLine("结果:{0}", task.Result); }
运行客户端可以正常获得结果,若使用FromUri,无法通过模型绑定验证,也无法获得结果。
改变客户端传值的方式:
HttpClient client = new HttpClient(); string url = "http://localhost/WebApi_Test/api/account/modelvalid?Field1Name=1name&Field2Name=2name"; using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url)) { HttpResponseMessage response = client.SendAsync(request).Result; Console.WriteLine("状态码:{0}",(int)response.StatusCode); var task = response.Content.ReadAsStringAsync(); task.Wait(); Console.WriteLine("结果:{0}", task.Result); }
运行结果为:
自定义模型绑定器
模型绑定器从值提供器(value provider)中获得原始输入,这种设计拆分出两个不同的功能:
1)值提供器使用HTTP请求并且填充一个词典。
2)模型绑定器使用这个词典填充模型。
默认的值提供器从请求URI的查询字符串和路由词典中获取值。要绑定的参数的名称保存在ModelBindingContext.ModelName属性中,模型绑定器在词典中找相应的键值对。如果键值对存在,并且能够转换为待处理模型,模型绑定器分配绑定值给ModelBindingContext.Model属性。模型绑定器不会限制简单类型的转换。自定义模型绑定器需要实现IModelBinder接口。
例:
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; } 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; } }
使用上述自定义的模型绑定器的方式有多种。
方式一、对于一个操作(Action)。
例:
public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)
方式二、对于一个控制器。
例:
[ModelBinder(typeof(GeoPointModelBinder))] public class GeoPoint { // .... }
方式三、注册模型绑定器后,依然要使用在操作上使用特性,不过不用指定类型
例:
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); // ... } } public HttpResponseMessage Get([ModelBinder] GeoPoint location) { ... }
自定义值提供器
模型绑定器从值提供器中获取值,自定义值提供器需要实现IValueProvider接口。
例:
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。
public class CookieValueProviderFactory : ValueProviderFactory { public override IValueProvider GetValueProvider(HttpActionContext actionContext) { return new CookieValueProvider(actionContext); } }
注册值提供器工厂。
public static void Register(HttpConfiguration config) { config.Services.Add(typeof(ValueProviderFactory), new CookieValueProviderFactory()); // ... }
使用值提供器工厂,指定使用CookieValueProvider。
public HttpResponseMessage Get( [ValueProvider(typeof(CookieValueProviderFactory))] GeoPoint location)
自定义HttpParameterBinding
ModelBinderAttribute继承自ParameterBindingAttribute,ParameterBindingAttribute继承自Attribute,ParameterBindingAttribute只有一个方法GetBinding,改=该方法返回HttpParameterBinding。HttpParameterBinding代表了参数与值之间的绑定关系。
public class ModelBinderAttribute : ParameterBindingAttribute {......} public abstract class ParameterBindingAttribute : Attribute { protected ParameterBindingAttribute(); // 获得参数绑定 // parameter:参数描述 public abstract HttpParameterBinding GetBinding(HttpParameterDescriptor parameter); }
例:利用请求头中的if-match或if-none-match获得ETags。
public class ETag { public string Tag { get; set; } } public enum ETagMatch { IfMatch, IfNoneMatch } 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; } }
为使用自定义的HttpParameterBinding,定义一个派生自ParameterBindingAttribute的类。
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) { } }
在控制器操作(Action)中使用它。
public HttpResponseMessage Get([IfNoneMatch] ETag etag) { ... }
另外一种使用自定义的HttpParameterBinding的方式是利用HttpConfiguration.ParameterBindingRules这个属性。
例:
config.ParameterBindingRules.Add(p => { if (p.ParameterType == typeof(ETag) && p.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Get)) { return new ETagParameterBinding(p, ETagMatch.IfNoneMatch); } else { return null; } });
可插拔服务IActionValueBinder
整个模型绑定过程是由IActionValueBinder服务控制器的。其默认实现完成以下工作:
1)在参数中查找ParameterBindingAttribute,包括[FromBody], [FromUri], and [ModelBinder], 或者自定义特性。
2)如果步奏1)中没有找到,那么在HttpConfiguration.ParameterBindingRules中寻找一个返回值为HttpParameterBinding的方法。
3)如果没有找到就使用默认规则。
如果操作(Action)参数是简单类型,Web API框架会从URI中获取值。简单类型是指:.NET 框架定义的原始类型(int, bool, double等)、TimeSpan、DateTime、Guid、decimal、string;另外还有包含类型转换器的类型,改转换器可将字符串转换为此类型。这里从URI获取值具体指:从路由词典中获取值或者从URI的查询字符串中获取值。具体过程见介绍路由那篇博文。对于复杂类型,Web API会使用多媒体格式化器从消息体中获得值。
参考:
https://docs.microsoft.com/en-us/aspnet/web-api/
部分示例自于该网站
转载与引用请注明出处。
时间仓促,水平有限,如有不当之处,欢迎指正。