今天跟大家分享下在Asp.NET Web API中Controller是如何解析从客户端传递过来的数据,然后赋值给Controller的参数的,也就是参数绑定和模型绑定。

Web API参数绑定就是简单类型的绑定,比如:string,char,bool,int,uint,byte,sbyte,short,ushort,long, float这些基元类型。模型绑定就是除此之外的复杂类型的绑定。大家都知道在MVC中模型绑定都是通过默认的DefaultModelBinder来绑定的,没有Get请求和POST请之分。然而在Web API中参数和模型绑定的机制在Get请求和POST请求是不一样的。

一:参数绑定(简单类型绑定)    

Web API参数绑定时,Action默认是从路由数据(url片段)和querystring中获取数据的。我们都知道,Get请求一个服务的时候,客户端是把数据放在URL中发送到服务器端的;而POST请求是把数据放到请求体(Request Body)发送到服务器端的。所以默认情况下在WebAPI中我们只能用GET请求去发送简单类型的数据到服务器端,然后Action再获取数据,举个栗子:

准备模型:

  public class Number
    {
        public int A { get; set; }
        public int B { get; set; }
        public Operation Operation { get; set; }
    }
    public class Operation
    {
        public string Add{get;set;}
        public string Sub { get; set; }
    }
View Code

配置路由:

      config.Routes.MapHttpRoute(
                name: "ActionApi",
                routeTemplate: "api/norestful/{controller}/{action}/{id}",
                defaults: new {id = RouteParameter.Optional}
                );
View Code

WebAPI Controller如下:

   public class ValuesController : ApiController
    {
        [HttpGet, HttpPost]
        public int SubNumber(int a,int b)
        {
            return a - b;
        }
}
View Code

客户端ajax调用如下:

    function  ajaxOp(url,type,data,contentType) {
        $.ajax({
            url: url,
            type: type,
            data: data,
            contentType:contentType
            success:function(result) {
                alert(result);
            }
        });
    }
    function a() {
        var data = { a: 1, b: 2 };
        ajaxOp('/api/norestful/Values/SubNumber', 'POST',data);
    }
View Code

输出结果如下:

 

如果换成POST请求,则找不到匹配的action

出现这种情况的原因主要是因为简单类型的绑定默认情况下是利用【FromUri】特性来解析数据的,光看名称就知道它只负责从URL中读取数据,在后面复杂类型绑定时会讲到【FromUri】的用法。

当然你要在POST请求下去绑定简单类型,也是可以的,有三种办法可以解决。

方法一:请求的数据以querystring的方式把数据放在URL中,POST空数据。

  function a() {
        var data = { a: 1, b: 2 };
        ajaxOp('/api/norestful/Values/SubNumber?'+$.param(data), 'POST');
}

方法二:手动从请求中读取数据:

  POST请求下:

[HttpPost]
        public async Task<IHttpActionResult> AddNumbers()  
        {
            if (Request.Content.IsFormData())
            {
                NameValueCollection values = await Request.Content.ReadAsFormDataAsync();
                int a, b;
                int.TryParse(values["a"], out a);
                int.TryParse(values["b"], out b);
                return Ok(a - b);
            }
            return StatusCode(HttpStatusCode.BadRequest);
        }
View Code

此方法能解决问题,但是和模型绑定无关,ReadAsFormDataAsync是HttpRequestMessage类HttpContent属性的一个扩展方法,该方法负责解析请求头中content-type类型为application/x-www-form-urlencoded类型中的数据。

  Get请求下:

  [HttpGet]
        public IHttpActionResult SubNumberByGet() 
        {
            Dictionary<string, string> dic = Request.GetQueryNameValuePairs().ToDictionary(x => x.Key, x => x.Value);
            int a, b;
            int.TryParse(dic["a"], out a);
            int.TryParse(dic["b"], out b);
            return Ok(a-b);
        }
View Code

方法三:利用【FromBody】特性,此特性将从请求体(Request body)中获取数据。但是【FromBody】特性只能用于Action中的一个参数,如果这样写:

  [HttpGet, HttpPost]
        public int SubNumber([FromBody]int a,[FromBody]int b)
        {
            return a - b;
        }

将会抛出无法将多个参数绑定到请求的异常:

所以在POST请求下绑定一个简单类型利用【FromBody】特性还是可以的,也是最常用的解决方案。

 

之所以应用【FromBody】特性绑定多个简单类型抛出异常的原因就是在Web API框架下,请求体(request body)中的数据会以stream流的方式发送到服务器端,而我们没办法在模型绑定系统读取stream流中数据后再去改变它;而不像在MVC框架下在请求开始之前请求体(RequestBody)中的数据被处理后以键值对的方式存储在内存当中,所以MVC Controller中绑定多个复杂类型是没有问题的。同样在Web API框架下默认一个Action也不能同时绑定多个复杂类型,这点后面会讲到,同时也会提供同时绑定多个复杂类型的相关解决方案。

 

在Web API框架下,参数绑定(简单类型绑定)读取数据有三种不同的方式:

1:Web API首先检测参数是否应用了【FromBody】特性,如果有,就从该特性直接从请求体中读取数据。

2:如果没有【FromBody】特性,Web API就从绑定规则中读取数据,绑定规则通过在WebApiConfig文件中设置HttpConfiguration的ParameterBindingRules属性来实现,比如:config.ParameterBindingRules.Insert(0, typeof(Number), o => o.BindWithAttribute(new FromUriAttribute())),指的就是在项目中Number类型默认都是通过【FromUri】特性来获取数据,这样不必显示提供【FromUri】特性了。

3:如果没有【FromBody】特性,没有绑定规则,则通过【FroUri】特性读取数据,这也是参数绑定的默认行为。

 

二:模型绑定(复杂类型绑定)

Web API复杂类型绑定时候,Action默认从请求体(request body)中获取数据,所以默认只能用POST请求去发送复杂类型到服务器端,举个栗子:

Action如下:

  [HttpGet,HttpPost]
        public int AddNumber(Number number)
        {
            return number.A + number.B;
        }

客户端ajax如下:

   function b() {
        var data = { a: 1, b: 2};
        ajaxOp('/api/norestful/Values/AddNumber', 'POST', data);
}

Action在默认POST请求下,只能绑定一个复杂类型,如果绑定多个复杂类型,将会抛出异常,原因前面已经提到过。如果要绑定多个复杂类型,至少有四个办法可以解决。

方法一:在GET请求下,所有类型应用【FromUri】特性。

Action如下:

   [HttpGet, HttpPost]
        public int OpNumbers([FromUri]Number number,[FromUri] Operation op)
        {
            return op.Add ? number.A + number.B : number.A - number.B;
        }

客户端ajax如下:

   function d() {
        var data = { a: 1, b: 2, add: true, sub: false }
        ajaxOp('/api/norestful/Values/OpNumbers', 'GET', data);
}

方法二:手动从请求中读取数据,具体实现方法跟上面简单类型手动从请求中读取数据的方法是一样的,就不多讲了。

方法三:在GET请求下利用嵌套复杂类型绑定数据,并应用【FromUri】特性
Action如下:

[HttpGet]
        public int OpNumbersByNestedClass([FromUri]Number number)
        {
            return number.Operation.Add ? number.A + number.B : number.A - number.B;
        }

客户端ajax如下:

function b2() {
        var data = { a: 1, b: 2, add: true, sub: false };
        ajaxOp('/api/norestful/Values/OpNumbersByNestedClass', 'GET', {
            'number.a': data.a,
            'number.b': data.b,
            'number.operation.add': data.add,
            'number.operation.sub': data.sub
        });
}

方法四:POST请求下:

Action如下:

  [HttpGet,HttpPost]
        public int OpNumbersByNestedClass(Number number)
        {
            return number.Operation.Add ? number.A + number.B : number.A - number.B;
        }

客户端ajax如下:

   function b4() {
        var data = { a: 1, b: 2, 'operation.add': true, 'operation.sub': false };
        ajaxOp('/api/norestful/Values/OpNumbersByNestedClass', 'POST', data);
    }

在POST请求下,复杂类型的属性必须加类型名称作为前缀,或者var data={a:1,b:2,operation:{add:true,sub:false}}这样声明,Action 参数才能获取到数据。

 

其实说了这么多,简单类型绑定和复杂类型绑定在本质上没什么太大的区别,真正的区别在于数据绑定是通过GET请求(简单类型的默认方式,复杂类型通过设置【FromUri】实现)还是POST请求( 复杂类型的默认方式,简单类型通过设置【Frombody】实现)实现的,说白了就是【FromUri】特性和【FromBody】特性之间的区别。现在就讲下这两个特性内部是如何找到数据的。

【FromUri】特性

应用【FromUri】特性,Web API Action中参数将从URL中解析数据,而数据解析是通过值提供程序工厂创建值提供程序来获取数据的,对于简单类型,值提供程序则获取Action参数名称和参数值;对于复杂类型,值提供程序则获取类型属性名称和属性值。

下面是Web API中值提供程序工厂类的代码:

namespace System.Web.Http.ValueProviders {
    public abstract class ValueProviderFactory {
        public abstract IValueProvider GetValueProvider(HttpActionContext context);
    }
}

ValueProviderFactory通过HttpActionContext参数来选择创建什么样的提供程序来解析数据。

【FromBody】特性

应用【Frombody】特性,Web API Action中参数将从请求体(Request Body),并且通过媒体类型格式化器获取和绑定数据,在Web API框架下有4中内置的媒体格式化器,分别是:

       1:JsonMediaTypeFormatter,对应的content-type是:application/json, text/json

       2:XmlMediaTypeFormatter,对应的content-type是:XmlMediaTypeFormatter

      3:FormUrlEncodedMediaTypeFormatter,对应的content-type是:对应的content-type是:application/x-www-form-urlencoded。

      4:JQueryMvcFormUrlEncodedFormatter,对应的content-type是:对应的content-type是:application/x-www-form-urlencoded。

在默认情况下POST请求采用JQueryMvcFormUrlEncodedFormatter来解析数据的,JQueryMvcFormUrlEncodedFormatter类通过模型绑定系统利用值提供程序从URL中读取数据,这里的值提供程序是NameValuePairsValueProvider类,该类实现IValueProvider接口来获取键值对中的数据。

 

当然,你也可以在客户端请求时指定请求的content-type类型,这样Web API会根据客户端的content-type来选择不同的媒体类型格式化器。如果客户端采用application/json格式来传输数据,Web API在后台则会采用JsonMediaTypeFormatter来解析数据。举个栗子,上面最后一个例子更改客户端ajax请求,Controller不变:

    function b4() {
        var data = { a: 1, b: 2, operation: { add: true, sub: false } };
        ajaxOp('/api/norestful/Values/OpNumbersByNestedClass', 'POST', JSON.stringify(data), 'application/json');
}

在下图我们可以看到数据请求格式。这种以Json数据格式传递到Action来处理模型绑定,相信在MVC中无处不在吧。

而默认采用JQueryMvcFormUrlEncodedFormatter,则content-type如下图所示:

好了,今天就到这里吧。