ASP.Net Web API 的参数绑定[翻译]

原文地址:Parameter Binding in ASP.NET Web API

译文如下:

当Web API相应Controller的一个方法时,它必定存在一个设置参数的过程,叫作数据绑定。这篇文章描述了Web API如何绑定参数以及如何自定义绑定过程。

一般情况下,Web API绑定参数符合如下规则:

    • 如果参数为简单类型,Web API 尝试从URI中获取。简单参数类型包含.Net源生类型(int,bool,double...),加上TimeSpan,DateTime,Guid,decimal和string.加上任何包含string转化器的类型。(More about type converters later.)
    • 对复杂类型来说,Web API 试图从message body 中读取,使用media-type 类型。

  典型Web API Controller方法的例子:

HttpResponseMessage Put(int id, Product item) { ... }

  参数id是简单类型,所以webapi试图从URI中获取值。参数item为复杂类型,所以web api 使用 media-type 类型从request body中读取。

  从URI中获取值,Web API会从路由或者URI的查询参数中获取。路由数据例子如下:"api/{controller}/public/{category}/{id}",更多细节可参考:Routing and Action Selection.

  在余下的文章中,我将给你介绍的是如何自定义参数绑定的过程。对于复杂类型,当然最好尽可能考虑media-type类型转化。一个http的关键原理是:资源是保存在message body 中被传递的,使用内容协商来指定资源如何表示。media-type 类型就是被设计来实现这个目标的。

使用[FromUri]

  在参数前添加[FromUri]属性可以强制Web API 从URI中读取复杂类型。下面的例子定义了GeoPoint类型,以及一个从URI中获取GeoPoint的controller方法。

public class GeoPoint
{
    public double Latitude { get; set; }
    public double Longitude { get; set; }
}

public ValuesController : ApiController
{
    public HttpResponseMessage Get([FromUri] GeoPoint location) { ... }
}

  客户端可以通过查询字符将两个参数传递给Web API。例子如下:

http://localhost/api/values/?Latitude=47.678558&Longitude=-122.130989

使用【FromBody】

  给参数添加[FromBody]属性可以迫使Web API从request body 中读取简单参数。

public HttpResponseMessage Post([FromBody] string name) { ... }

  在这个例子中WebAPI将使用一个 media-type 转化器从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]后Web API通过Content-Type header选择格式。在这个例子中,content type 是“application/json” 并且request body是原始的Json字符串(不是Json对象)。

  有而且只有一个参数允许从message body 中读取,下面的例子将不会成功:

// Caution: Will not work!
public HttpResponseMessage Post([FromBody] int id, [FromBody] string name) { ... }

  这条规则的原因为:request body 可能存储在一个只能读取一次的非缓冲的数据流中。

Type Converters

  创建一个TypeConverter并提供一个字符串转化,你就可以使Web API像对待一个简单类型那样对待一个类(所以Web API会尝试从URI中获取绑定的数据)。

   下面的代码展示了一个GeoPoint类代表地理坐标,添加TypeConvert方法将字符串转化为GeoPoint实例。GeoPoint类添加了[TypeConvert]属性,制定类型转化器。(这个例子取自Mike Stall的博客文章How to bind to custom objects in action signatures in MVC/WebAPI)

[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);
    }
}

  现在Web API将GeoPoint视为简单类型,也就是说他会试图从URL中获取GeoPoint参数,而不用在参数前添加[FromUri]

public HttpResponseMessage Get(GeoPoint location) { ... }

  客户端请求的URI就像这样:

http://localhost/api/values/?location=47.678558,-122.130989

Model Binders

  操控类型转化更强的是创建自定义对象绑定Model Binder。使用Model Binder,你可以接收一个Http请求,一个Action和一个路由数据的原始值。

  创建一个Model Binder,需要继承IModelBinder接口,这个接口只定义了一个方法BindModel

bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);

  下面是GeoPoint对象的Model Binder

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;
    }
}

  Model Binder 从Value Provider获取原始输入,这个设计中区分了两个单独的功能:

    • Value Provider提供了一个Http请求并填充到一个键值字典中。
    • Model Binder使用这个字典填充Model。

  WebAPI默认的Value Provider通过路由数据和URI的查询参数获取数据。如果URI是http://localhost/api/values/1?location=48,-122,Value Provider创建一个键值对:

    • id="1"
    • location="48,122"

  (我假设默认的路由模板是"api/{controller}/{id}")

  绑定参数的名称存储在ModelBindingContext.ModelName属性中,Model Binder在字典中查找Key和它的值。如果值存在而且可以被转化成GeoPoint,Model Binder将绑定的值分配给ModelBindingContext.Model属性。

  注意:Model Binder并不只限定于简单类型。在这个例子中,Model Binder首先寻找location类型的列表,如果失败了才使用type Conversion。

设置 Model Binder

  有多种路径可以设置Model Binder。首先,你可以给参数添加[ModelBinder]属性。

public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)

  你也可以在类型上添加[ModelBinder]属性。Web API会对所有此类型的参数指派Model Binder。

[ModelBinder(typeof(GeoPointModelBinder))]
public class GeoPoint
{
    // ....
}

  最后,你也可以在HttpConfiguration中添加Model-binder Provider。model-binder provider是一个创建Model binder的简单的工厂类。你可以通过继承 ModelBinderProvider类来创建一个Provider.然而,如果你的model binder只处理一个类型,简单的方法是嵌入SimpleModelBinderProvider,这也是SimpleModelBinderProvider的设计目的。下面的代码展示了如何做到:

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);

        // ...
    }
}

  使用model-binding provider,你仍然需要给参数添加[ModelBinder]属性,去告诉WebAPI它应该使用model binder并且不是一个media-type类型。但是现在你不用在属性中指定model binder的类型:

public HttpResponseMessage Get([ModelBinder] GeoPoint location) { ... }

  

Value Providers

  我提到了model binder从Value provider中获取值。通过实现IValueProvider接口创建自定义value provider。下面的列子展示了如何从一个Request请求的Cookie中拉取值:

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类的Value Provider工厂。

public class CookieValueProviderFactory : ValueProviderFactory
{
    public override IValueProvider GetValueProvider(HttpActionContext actionContext)
    {
        return new CookieValueProvider(actionContext);
    }
}

  把Value Provider添加到HttpConfiguration

public static void Register(HttpConfiguration config)
{
    config.Services.Add(typeof(ValueProviderFactory), new CookieValueProviderFactory());

    // ...
}

  Web API中包含所有的Value Provider,当我们调用ValueProvider.GetValue方法时,model binder接收到的是第一个可以产生它的Provider。

  另外一种方法是,你可以使用ValueProvider属性,在参数级下设置ValueProvider 工厂,就像下面所示:

public HttpResponseMessage Get(
    [ValueProvider(typeof(CookieValueProviderFactory))] GeoPoint location)

  这就会告诉Web API使用指定的value provider factory工厂,而不是其他注册的value provider。

HttpParameterBinding

  模型绑定是一个普通机制中更常用的一个具体实例。如果你查看[ModelBinder]属性,你就会发现他继承于一个静态类ParameterBindingAttribute。这个静态类只定义了一个方法:GetBinding。这个方法返回一个HttpParameterBinding对象。

public abstract class ParameterBindingAttribute : Attribute
{
    public abstract HttpParameterBinding GetBinding(HttpParameterDescriptor parameter);
}

  HttpParameterBinding 负责将绑定参数转化为值。如果设置了 [ModelBinder],属性会返回一个实现了IModelBinder 接口的HttpParameterBinding 去实现实际的绑定。你也可以实现你自己的HttpParameterBinding

  举例来说:假设你想从一个请求的headers中的if-match和if-none-match中获取ETags.首先我们需要定义ETag类。

public class ETag
{
    public string Tag { get; set; }
}

  再次我们需要定义一个枚举来确定是从header中的if-match还是if-none-match中取得ETag。

public enum ETagMatch
{
    IfMatch,
    IfNoneMatch
}

  下面是一个从我们需要的header中获取ETag并且把它绑定到ETag类型参数的HttpParameterBinding类的例子:

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 字典中。

  NOTE:如果你的ExecuteBindingAsync方法从请求连接中的Body读取值。需要重写WillReadBody 属性并让他返回true。因为请求的Body可能是只能读一次的无缓冲数据流。所以Web API定义了强制执行的规则:只有一个绑定可以读取请求的body。

  为了实现一个自定义HttpParameterBinding。你可以定义一个继承自ParameterBindingAttribute的属性。在ETagParameterBinding中定义了两个属性,一个是为if-match headers 另一个是为if-not-match headers。两个都继承自静态基类。

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)
    {
    }
}

  下例说明了在controller的方法中如何使用[IfNoneMatch]属性:

public HttpResponseMessage Get([IfNoneMatch] ETag etag) { ... }

  除了ParameterBindingAttribute,这里还有另外一种关联自定义HttpParameterBinding的方法。在HttpConfiguration 对象中,ParameterBindingRules 属性是一个(HttpParameterDescriptor -> HttpParameterBinding)类型的匿名函数的集合。举例来说,你可以添加一个规则,所有的包含ETag参数的Get方法使用if-none-match的ETagParameterBinding:

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

  整个参数绑定的过程被一个名为IActionValueBinder的可插拔的服务控制。IActionValueBinder的默认实现方式遵循以下步骤:

    1. 在参数中寻找ParameterBindingAttribute 。包含[FromBody][FromUri], 和[ModelBinder]或自定义的属性。
    2. 否则,从HttpConfiguration.ParameterBindingRules中寻找一个返回非空的HttpParameterBinding的函数。
    3. 否则,使用上面所描述的默认规则。
      • 如果参数简单或者有类型转化器,绑定在URI上。这就相当于给参数添加了 [FromUri]属性。
      • 否则,尝试从消息体中读取参数。相当于给参数添加了[FromBody] 属性。

  如果你想,你可以使用自定义的实现代替整个IActionValueBinder 服务。

相关资源:

  Custom Parameter Binding Sample

  Mike Stall谢了非常好的一系列的关于Web API 参数绑定的blog文章:

时间: 06-03

ASP.Net Web API 的参数绑定[翻译]的相关文章

ASP.NET Web API Model-ModelBinder

ASP.NET Web API Model-ModelBinder 前言 本篇中会为大家介绍在ASP.NET Web API中ModelBinder的绑定原理以及涉及到的一些对象模型,还有简单的Model绑定示例,在前面的篇幅中讲解了Model元数据.ValueProvider的模块,然后还有本篇的Model绑定的模块这些会结合到后面篇幅中的ParameterBinder模块中来使用,也就是说在ASP.NET Web API框架中绑定的方式有两种实现,都是通过ParameterBinder来对参

ASP.NET Web API Model-ParameterBinding

ASP.NET Web API Model-ParameterBinding 前言 通过上个篇幅的学习了解Model绑定的基础知识,然而在ASP.NET Web API中Model绑定功能模块并不是被直接调用的,而是要通过本篇要介绍的内容ParameterBinding的一系列对象对其进行封装调用,通过本篇的学习之后也会大概的清楚在Web API中绑定会有哪几种方式. Model-ParameterBinding(对象篇) 在ASP.NET Web API中ParameterBinding代表着

ASP.NET Web API - ASP.NET MVC 4 系列

       Web API 项目是 Windows 通信接口(Windows Communication Foundation,WCF)团队及其用户激情下的产物,他们想与 HTTP 深度整合.WCF 进行 Web 服务编程的迭代是一个抽象事务,主要为了隐藏像传输细节一样的内容.Web API 试图彻底颠覆这一过程,去掉 WCF 中的大部分层,而允许开发人员直接访问 HTTP 编程模型的所有方面.        ASP.NET MVC 在接收表单数据生成 HTML 方面功能非常强大:ASP.NE

使用OWIN 构建自宿主ASP.NET Web API 2

--Use OWIN to Self-Host ASP.NET Web API 2 原文链接:http://www.asp.net/web-api/overview/hosting-aspnet-web-api/use-owin-to-self-host-web-api 摘要:ASP.NET Web API 2拥有符合RESTFUL风格,原生支持HTML协议,解耦IIS和windows server服务器等诸多优秀特性.本文讲述如何使用OWIN构建ASP.NET Web API 2.在翻译的基础

ASP.NET Web API中的参数绑定总结

ASP.NET Web API中的action参数类型可以分为简单类型和复杂类型. HttpResponseMessage Put(int id, Product item) id是int类型,是简单类型,item是Product类型,是复杂类型. 简单类型实参值从哪里读取呢?--一般从URI中读取 所谓的简单类型包括哪些呢?--int, bool, double, TimeSpan, DateTime, Guid, decimal, string,以及能从字符串转换而来的类型 复杂类型实参值从

【翻译】ASP.NET Web API是什么?

参考页面: http://www.yuanjiaocheng.net/entity/linq-to-entities-projection.html http://www.yuanjiaocheng.net/entity/dbset-class.html http://www.yuanjiaocheng.net/entity/dbentityentry-class.html http://www.yuanjiaocheng.net/entity/change-tracking.html http

[转]让ASP.NET Web API支持$format参数的方法

本文转自:http://www.cnblogs.com/liuzhendong/p/4228592.html 在不使用OData的情况下,也可以让ASP.NET Web API支持$format参数,只要在WebApiConfig里添加如下三行红色粗体代码即可: using System; using System.Collections.Generic; using System.Linq; using System.Web.Http; using System.Net.Http.Format

ASP.NET Web API实践系列07,获取数据, 使用Ninject实现依赖倒置,使用Konockout实现页面元素和视图模型的双向绑定

本篇接着上一篇"ASP.NET Web API实践系列06, 在ASP.NET MVC 4 基础上增加使用ASP.NET WEB API",尝试获取数据. 在Models文件夹下创建Comment类: namespace MvcApplication5.Models { public class Comment { public int ID { get; set; } public string Author { get; set; } public string Text { ge

让ASP.NET Web API支持$format参数的方法

在不使用OData的情况下,也可以让ASP.NET Web API支持$format参数,只要在WebApiConfig里添加如下三行红色粗体代码即可: using System; using System.Collections.Generic; using System.Linq; using System.Web.Http; using System.Net.Http.Formatting; namespace ProjectManagementWebAppV4 { public stat