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/

部分示例自于该网站

---------------------------------------------------------------------

转载与引用请注明出处。

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏文武兼修ing——机器学习与IC设计

Python解决大规模二进制数据错位问题描述解决方法实验代码最终代码

问题描述 有一些二进制数据,每八位按顺序存为一个十进制数保存成CSV文件,每行为一个二进数数据,每个单元格均为一个十进制数。若数据为0000 0001 1000...

34510
来自专栏Spark生态圈

[spark] Shuffle Write解析 (Sort Based Shuffle)

从 Spark 2.0 开始移除了Hash Based Shuffle,想要了解可参考Shuffle 过程,本文将讲解 Sort Based Shuffle。

1412
来自专栏DOTNET

ASP.NET Web API编程——序列化与内容协商

1 多媒体格式化器 多媒体类型又叫MIME类型,指示了数据的格式。在HTTP协议中多媒体类型描述了消息体的格式。一个多媒体类型包括两个字符串:类型和子类型。 例...

3866
来自专栏FreeBuf

浅析ReDoS的原理与实践

*本文原创作者:MyKings,本文属FreeBuf原创奖励计划,未经许可禁止转载 ReDoS(Regular expression Denial of Ser...

5935
来自专栏草根专栏

.NET Core装饰模式和.NET Core的Stream

Beverage是所有咖啡饮料的抽象类, 里面的cost方法是抽象的. description变量在每个子类里面都需要设置(表示对咖啡的描述).

42113
来自专栏恰同学骚年

.NET基础拾遗(4)委托、事件、反射与特性

  委托这个概念对C++程序员来说并不陌生,因为它和C++中的函数指针非常类似,很多码农也喜欢称委托为安全的函数指针。无论这一说法是否正确,委托的的确确实现了和...

882
来自专栏葡萄城控件技术团队

如何在保留装箱对象的前提下修改值

有人问如何在保留装箱对象的前提下修改值? 场景: object obj = 100; Console.WriteLine("original object va...

2057
来自专栏跟着阿笨一起玩NET

Razor 语法快速参考 Razor 语法快速参考

本文引自:http://haacked.com/archive/2011/01/06/razor-syntax-quick-reference.aspx

1812
来自专栏草根专栏

用.NET Core实现装饰模式和.NET Core的Stream简介

该文章综合了几本书的内容. 某咖啡店项目的解决方案 ? 某咖啡店供应咖啡, 客户买咖啡的时候可以添加若干调味料, 最后要求算出总价钱. Beverage是所有咖...

6325
来自专栏DOTNET

.Net多线程编程—Parallel LINQ、线程池

Parallel LINQ 1 System.Linq.ParallelEnumerable 重要方法概览: 1)public static ParallelQ...

2967

扫码关注云+社区