【问题标题】:WebApi - Bind from both Uri and BodyWebApi - 从 Uri 和 Body 绑定
【发布时间】:2013-07-15 02:21:24
【问题描述】:

是否可以同时从 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/products/1

JSON 正文为:

{
    "Name": "Product Name"
}

将导致UpdateProduct 模型填充Id = 1Name = "Product Name"

更新

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

public HttpResponseMessage Put(int id, UpdateProduct model)
{

}

但是正如问题中所述,我特别想绑定到单个模型对象

我也把这个问题发到WebApi Codeplex discussion forum

【问题讨论】:

  • 如果您从 UpdateProduct 中删除 Id 并将其添加到您的操作签名中:public HttpResponseMessage Put(int id, UpdateProduct model) 它也可以在没有任何自定义模型绑定器的情况下工作。
  • 看看这篇文章,这似乎是你需要的:blogs.msdn.com/b/jmstall/archive/2012/04/18/…
  • 您找到解决方案了吗?我有同样的问题。在我看来,这是一种非常愚蠢和不直观的行为。
  • 完全不直观的行为...尤其是它只适用于 MVC...

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


【解决方案1】:

这是 odyth 答案的改进版本:

  1. 也适用于无实体请求,并且
  2. 从查询字符串和路由值中获取参数。

为简洁起见,我只是发布了 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]);
}

【讨论】:

  • 这看起来很棒。这段代码是否完成了默认 'FromBody' 'FromUri' 属性组合起来的所有功能?另外,您知道 ASP.NET 团队决定不包括开箱即用的“FromBodyAndUri”之类的内容的原因是什么?我浏览了无数博客文章,但无法真正找到为什么这是不好的做法……我所有的控制器都接受IRequest&lt;&gt; (Jimmy Bogard's Mediator pattern) 的实现,它包含一个对象中的所有参数。能够用 body 和 uri 参数填充它会很棒。
  • 谢谢!我将它与自定义 ParameterBindingAttribute 结合使用,像这样使用 public IHttpActionResult Put([FromUriAndBody]ComplexType param)
  • Konamiman - System.Web.HttpUtility 需要对 System.Web 的引用,这在 Web API 项目中是多余的。建议使用System.Net.Http.UriExtensions.ParseQueryString()。所以 QueryStringValues() 中的代码应该是这样的:var queryStringValues = request.RequestUri.ParseQueryString(); ...
  • @SimonGates 可以分享一下你的绑定属性代码吗?
  • @Langdon 是的,当然,我会尽量记住明天到办公室时提出要点。
【解决方案2】:

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

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

更新:

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

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

Request.cs

public abstract class Request : IValidatableObject
{
    public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        yield return ValidationResult.Success;
    }
}

【讨论】:

  • 嗨,看起来是一个很好的解决方案,我已经在我们的 API 中使用了你的建议,但我遇到了一些问题,值仍然为空。我的猜测是我在 WebAPIconfig 中做错了什么。我的代码是codeconfig.ParameterBindingRules.Insert(0, descriptor => descriptor.ParameterType.IsSubclassOf(typeof(BaseApiController)) ? new BodyAndUriParameterBinding(descriptor) : null); code
  • @Gregory 刚刚更新了我的答案以使其更加清晰。我所有的模型都从Request 扩展而来,你说你所有的模型都从BaseApiController 扩展而来,如果这是真的,那将是疯狂的。确保所有模型都从上面的请求类扩展,并将 descriptor.ParameterType.IsSubclassOf(typeof(BaseApiController)) 更改为 descriptor.ParameterType.IsSubclassOf(typeof (Request))
【解决方案3】:

好吧,我想出了一个办法。基本上,我做了一个动作过滤器,它将在模型从 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));
                }
            }
        }
    }
}

【讨论】:

  • 如果您有其他 ActionFilterAttributes 依赖于完全绑定的模型,则此解决方案将不起作用,因为 ActionFilterAttributes 没有保证的操作顺序。
  • 什么是 SetValueFast?
【解决方案4】:

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

    [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 with body:{"Name":"tool"}

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

【讨论】:

  • Felix - 我相信它适用于 MVC,但我正试图让它适用于 WebApi 项目
  • 对,我的错,没有使用 web api,所以不能说,我相信你已经看到了这个blogs.msdn.com/b/jmstall/archive/2012/04/16/…。据我了解,您需要创建自定义格式化程序来实现这一目标
猜你喜欢
  • 2013-08-08
  • 1970-01-01
  • 1970-01-01
  • 2017-04-12
  • 1970-01-01
  • 2018-02-12
  • 1970-01-01
  • 2013-05-15
  • 2014-05-31
相关资源
最近更新 更多