ASP.NET Web API编程——模型验证与绑定

时间:2022-08-16 05:19:07

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 = , 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);
}

输出结果:

ASP.NET Web API编程——模型验证与绑定

服务端运行截图:

ASP.NET Web API编程——模型验证与绑定

若客户端传值为:var cont = new { Id = 1, Field1Name = "1name", Field2Name="2name" };

ASP.NET Web API编程——模型验证与绑定

默认赋值

Web API会对客户端未指定的模型属性赋初值。对于int,double等数值类型默认的初值为0,对于字符串或引用类型默认的初值是null。如果未对属性使用特性加以约束,那么ModelState.IsValid的值就是true,若对这样的属性应用Required特性,那么当客户端为对其赋初值时,验证将无法通过,即ModelState.IsValid的值为false。

例:

上例中不对Id属性赋值,运行客户端结果为:

ASP.NET Web API编程——模型验证与绑定

可见框架自动为int型的Id赋初值0。

过载

此外当客户端所用实体属性多于服务端时,服务端会忽略多出来的属性,但建议控制器操作(Action)所用参数列表的参数或类属性与客户端所传参数完全匹配。

例:

若使用上述客户端,但传值为

var cont = new { Field1Name = "1name", Field2Name = "2name",FieldOverLoad ="overload"};

其中DataModel不包含FieldOverLoad 字段。

运行结果如下:

ASP.NET Web API编程——模型验证与绑定

过滤验证结果

可以自定义操作过滤器来统一处理模型验证失败的情形。自定义操作过滤器派生自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 != )
{
return false;
} double latitude, longitude;
if (double.TryParse(parts[], out latitude) &&
double.TryParse(parts[], 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);
}

运行结果为:

ASP.NET Web API编程——模型验证与绑定

自定义模型绑定器

模型绑定器从值提供器(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), , 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/

部分示例自于该网站

转载与引用请注明出处。

时间仓促,水平有限,如有不当之处,欢迎指正。