WebApi - 从Uri和Body中绑定

时间:2013-07-15 02:21:25

标签: c# asp.net-web-api

是否可以从Uri和Body绑定模型?

例如,给出以下内容:

routes.MapHttpRoute(
    name: "API Default",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

public class ProductsController : ApiController
{
    public HttpResponseMessage Put(UpdateProduct model)
    {

    }
}

public class UpdateProduct 
{
    int Id { get; set;}
    string Name { get; set; }
}

是否可以创建自定义活页夹以便PUT

  

/ API /产品/ 1

使用JSON正文:

{
    "Name": "Product Name"
}

会导致UpdateProduct模型填充Id = 1Name = "Product Name"吗?

更新

我知道我可以将动作签名更改为

public HttpResponseMessage Put(int id, UpdateProduct model)
{

}

然而,如问题中所述,我特别想要绑定到单个模型对象

我还将此问题发布到WebApi Codeplex discussion forum

4 个答案:

答案 0 :(得分:13)

以下是odyth答案的改进版本:

  1. 也适用于脱胎请求,
  2. 除路由值外,还从查询字符串中获取参数。
  3. 为简洁起见,我只发布了ExecuteBindingAsyncCore方法和一个新的辅助方法,其余的类是相同的。

    private async Task ExecuteBindingAsyncCore(ModelMetadataProvider metadataProvider, HttpActionContext actionContext,
            HttpParameterDescriptor paramFromBody, Type type, HttpRequestMessage request, IFormatterLogger formatterLogger,
            CancellationToken cancellationToken)
    {
        var model = await ReadContentAsync(request, type, Formatters, formatterLogger, cancellationToken);
    
        if(model == null) model = Activator.CreateInstance(type);
    
        var routeDataValues = actionContext.ControllerContext.RouteData.Values;
        var routeParams = routeDataValues.Except(routeDataValues.Where(v => v.Key == "controller"));
        var queryStringParams = new Dictionary<string, object>(QueryStringValues(request));
        var allUriParams = routeParams.Union(queryStringParams).ToDictionary(pair => pair.Key, pair => pair.Value);
    
        foreach(var key in allUriParams.Keys) {
            var prop = type.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public);
            if(prop == null) {
                continue;
            }
            var descriptor = TypeDescriptor.GetConverter(prop.PropertyType);
            if(descriptor.CanConvertFrom(typeof(string))) {
                prop.SetValue(model, descriptor.ConvertFromString(allUriParams[key] as string));
            }
        }
    
        // Set the merged model in the context
        SetValue(actionContext, model);
    
        if(BodyModelValidator != null) {
            BodyModelValidator.Validate(model, type, metadataProvider, actionContext, paramFromBody.ParameterName);
        }
    }
    
    private static IDictionary<string, object> QueryStringValues(HttpRequestMessage request)
    {
        var queryString = string.Join(string.Empty, request.RequestUri.ToString().Split('?').Skip(1));
        var queryStringValues = System.Web.HttpUtility.ParseQueryString(queryString);
        return queryStringValues.Cast<string>().ToDictionary(x => x, x => (object)queryStringValues[x]);
    }
    

答案 1 :(得分:7)

您可以定义自己的DefaultActionValueBinder。然后你可以从身体和uri混合搭配。这是一篇博客文章,其中包含一个用于Web Api的MvcActionValueBinder示例。使自己的DefaultActionValueBinder成为首选解决方案,因为它保证在执行任何其他ActionFilterAttribute之前绑定器已完成。

http://blogs.msdn.com/b/jmstall/archive/2012/04/18/mvc-style-parameter-binding-for-webapi.aspx

<强>更新

我在博客文章中的实现遇到了一些麻烦,并试图让它使用我的自定义媒体格式化程序。幸运的是,我的所有请求对象都从基类扩展而来,所以我创建了自己的格式化程序。

WebApiConfig中的

config.ParameterBindingRules.Insert(0, descriptor => descriptor.ParameterType.IsSubclassOf(typeof (Request)) ? new BodyAndUriParameterBinding(descriptor) : null);

BodyAndUriParameterBinding.cs

public class BodyAndUriParameterBinding : HttpParameterBinding
{
    private IEnumerable<MediaTypeFormatter> Formatters { get; set; }
    private IBodyModelValidator BodyModelValidator { get; set; }
    public BodyAndUriParameterBinding(HttpParameterDescriptor descriptor)
        : base (descriptor)
    {
        var httpConfiguration = descriptor.Configuration;
        Formatters = httpConfiguration.Formatters;
        BodyModelValidator = httpConfiguration.Services.GetBodyModelValidator();
    }

    private Task<object> ReadContentAsync(HttpRequestMessage request, Type type,
        IEnumerable<MediaTypeFormatter> formatters, IFormatterLogger formatterLogger, CancellationToken cancellationToken)
    {
        var content = request.Content;
        if (content == null)
        {
            var defaultValue = MediaTypeFormatter.GetDefaultValueForType(type);
            return defaultValue == null ? Task.FromResult<object>(null) : Task.FromResult(defaultValue);
        }

        return content.ReadAsAsync(type, formatters, formatterLogger, cancellationToken);
    }

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext,
        CancellationToken cancellationToken)
    {
        var paramFromBody = Descriptor;
        var type = paramFromBody.ParameterType;
        var request = actionContext.ControllerContext.Request;
        var formatterLogger = new ModelStateFormatterLogger(actionContext.ModelState, paramFromBody.ParameterName);
        return ExecuteBindingAsyncCore(metadataProvider, actionContext, paramFromBody, type, request, formatterLogger, cancellationToken);
    }

    // Perf-sensitive - keeping the async method as small as possible
    private async Task ExecuteBindingAsyncCore(ModelMetadataProvider metadataProvider, HttpActionContext actionContext,
        HttpParameterDescriptor paramFromBody, Type type, HttpRequestMessage request, IFormatterLogger formatterLogger,
        CancellationToken cancellationToken)
    {
        var model = await ReadContentAsync(request, type, Formatters, formatterLogger, cancellationToken);

        if (model != null)
        {
            var routeParams = actionContext.ControllerContext.RouteData.Values;
            foreach (var key in routeParams.Keys.Where(k => k != "controller"))
            {
                var prop = type.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public);
                if (prop == null)
                {
                    continue;
                }
                var descriptor = TypeDescriptor.GetConverter(prop.PropertyType);
                if (descriptor.CanConvertFrom(typeof(string)))
                {
                    prop.SetValue(model, descriptor.ConvertFromString(routeParams[key] as string));
                }
            }
        }

        // Set the merged model in the context
        SetValue(actionContext, model);

        if (BodyModelValidator != null)
        {
            BodyModelValidator.Validate(model, type, metadataProvider, actionContext, paramFromBody.ParameterName);
        }
    }
}

答案 2 :(得分:5)

好吧,我想出了办法。基本上,我创建了一个动作过滤器,它将在从JSON填充模型后运行。然后,它将查看URL参数,并在模型上设置适当的属性。完整来源如下:

using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;


public class UrlPopulatorFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var model = actionContext.ActionArguments.Values.FirstOrDefault();
        if (model == null) return;
        var modelType = model.GetType();
        var routeParams = actionContext.ControllerContext.RouteData.Values;

        foreach (var key in routeParams.Keys.Where(k => k != "controller"))
        {
            var prop = modelType.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public);
            if (prop != null)
            {
                var descriptor = TypeDescriptor.GetConverter(prop.PropertyType);
                if (descriptor.CanConvertFrom(typeof(string)))
                {
                    prop.SetValueFast(model, descriptor.ConvertFromString(routeParams[key] as string));
                }
            }
        }
    }
}

答案 3 :(得分:0)

如果我理解你,这应该是开箱即用的,例如:这对我有用:

    [HttpPost]
    public ActionResult Test(TempModel model)
    {
        ViewBag.Message = "Test: " + model.Id +", " + model.Name;

        return View("About");
    }

public class TempModel
{
    public int Id { get; set; }
    public string Name { get; set; }
}

routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );

并在请求上:localhost:56329 / Home / Test / 22 与身体:{“名称”:“工具”}

我将模型的属性设置为22和“工具”。