属性路由值到 Model/FromBody 参数

Attribute Routing Values into Model/FromBody Parameter

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

示例代码 post 到此:

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

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

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

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

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

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

{
  "name": "Testing Prodcu"
}

引用这篇文章Parameter Binding in ASP.NET Web API

模型绑定器

A more flexible option than a type converter is to create a custom model binder. With a model binder, you have access to things like the HTTP request, the action description, and the raw values from the route data.

To create a model binder, implement the IModelBinder interface

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

设置模型绑定器

There are several ways to set a model binder. First, you can add a [ModelBinder] attribute to the parameter.

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

You can also add a [ModelBinder] attribute to the type. Web API will use the specified model binder for all parameters of that type.

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

如果您不想创建自定义参数绑定器,您可能需要考虑不要将 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

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

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