属性路由值到Model / FromBody参数

时间:2016-07-19 13:57:43

标签: c# asp.net-web-api asp.net-web-api2 model-binding

在Web API(2)中使用属性路由时,我希望能够自动从URL获取路径参数到模型参数。这样做的原因是我的验证是在过滤器到达操作之前在过滤器中执行的,如果没有这个,则无法轻易获得其他信息。

考虑以下简化示例:

public class UpdateProductModel
{
    public int ProductId { get; set; }
    public string Name { get; set; }
}

public class ProductsController : ApiController 
{
    [HttpPost, Route("/api/Products/{productId:int}")]
    public void UpdateProduct(int productId, UpdateProductModel model) 
    {
         // model.ProductId should == productId, but is default (0)
    }
}

发布到此的示例代码:

$.ajax({
    url: '/api/Products/5',
    type: 'POST',
    data: {
        name: 'New Name'   // NB: No ProductId in data
    }
});

我希望在输入操作方法之前从路径参数填充模型中的ProductId字段(即,我的验证器可以使用它)。

我不确定模型绑定过程的哪一部分我需要在这里尝试和覆盖 - 我认为这是处理[FromBody]部分(这是模型参数)的位在这个例子中)。

在动作本身(例如model.ProductId = productId)中设置它是不可接受的,因为我需要在动作到达动作之前设置它。

4 个答案:

答案 0 :(得分:1)

参考这篇文章Parameter Binding in ASP.NET Web API

模型粘合剂

  

比类型转换器更灵活的选项是创建自定义   模型活页夹。使用模型绑定器,您可以访问类似的东西   HTTP请求,操作描述和来自的原始值   路线数据。

     

要创建模型绑定器,请实现IModelBinder接口

这是UpdateProductModel个对象的模型绑定器,它将尝试提取路径值并使用找到的任何匹配属性为模型提供水合。

public class UpdateProductModelBinder : IModelBinder {

    public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext) {
        if (!typeof(UpdateProductModel).IsAssignableFrom(bindingContext.ModelType)) {
            return false;
        }

        //get the content of the body and convert it to model
        object model = null;

        if (actionContext.Request.Content != null)
            model = actionContext.Request.Content.ReadAsAsync(bindingContext.ModelType).Result;

        model = model ?? bindingContext.Model
            ?? Activator.CreateInstance(bindingContext.ModelType);

        // check values provided in the route or query string 
        // for matching properties and set them on the model. 
        // NOTE: this will override any existing value that was already set.
        foreach (var property in bindingContext.PropertyMetadata) {
            var valueProvider = bindingContext.ValueProvider.GetValue(property.Key);
            if (valueProvider != null) {
                var value = valueProvider.ConvertTo(property.Value.ModelType);
                var pInfo = bindingContext.ModelType.GetProperty(property.Key);
                pInfo.SetValue(model, value, new object[] { });
            }
        }

        bindingContext.Model = model;

        return true;
    }
}

设置模型活页夹

  

有几种方法可以设置模型装订器。首先,你可以添加一个   [ModelBinder]属性为参数。

public HttpResponseMessage UpdateProduct(int productId, [ModelBinder(typeof(UpdateProductModelBinder))] UpdateProductModel model)
  

您还可以为该类型添加[ModelBinder]属性。 Web API会   将指定的模型绑定器用于该类型的所有参数。

[ModelBinder(typeof(UpdateProductModelBinder))]
public class UpdateProductModel {
    public int ProductId { get; set; }
    public string Name { get; set; }
}

给出以下模型和ModelBinder的简化示例

public class ProductsController : ApiController {
    [HttpPost, Route("api/Products/{productId:int}")]
    public IHttpActionResult UpdateProduct(int productId, UpdateProductModel model) {
        if (model == null) return NotFound();
        if (model.ProductId != productId) return NotFound();

        return Ok();
    }
}

以下集成测试用于确认所需的功能

[TestClass]
public class AttributeRoutingValuesTests {
    [TestMethod]
    public async Task Attribute_Routing_Values_In_Url_Should_Bind_Parameter_FromBody() {
        var config = new HttpConfiguration();
        config.MapHttpAttributeRoutes();

        using (var server = new HttpTestServer(config)) {

            var client = server.CreateClient();

            string url = "http://localhost/api/Products/5";
            var data = new UpdateProductModel {
                Name = "New Name" // NB: No ProductId in data
            };
            using (var response = await client.PostAsJsonAsync(url, data)) {
                Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
            }
        }
    }
}

答案 1 :(得分:0)

如果您不想创建自定义参数活页夹,可能需要考虑不将FromBody混合到FromUrl。而是完全使用FromUrl ......

[HttpPost, Route("/api/Products/{productId:int}/{name:string}")]
public void UpdateProduct([FromUri]UpdateProductModel model) 
{

}

或完全使用FromBody ......

[HttpPost, Route("/api/Products")]
public void UpdateProduct([FromBody]UpdateProductModel model) 
{

}

并相应地更新javascript

答案 2 :(得分:0)

由于这是更新,因此应为HttpPut。 PUT动词是幂等的,因此对API(相同的json请求)的任何后续请求应该具有相同的响应/效果(在服务器端没有创建资源)。应该在调用客户端中设置modelId模型。

public class ProductsController : ApiController 
{
    [HttpPut, Route("/api/Products/{productId:int}")]
    public void UpdateProduct(UpdateProductModel model) 
    {
         if (ModelState.IsValid)
         {
            //
         }
         else
         {
            BadRequest();
         }
    }
}

答案 3 :(得分:-1)

我没有看到任何问题。我能够从Uri看到productId。

我在Postman中尝试过POST到Uri:http://localhost:42020/api/products/1和json请求:

{
  "name": "Testing Prodcu"
}