Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Attribute Routing Values into Model/FromBody Parameter

When using attribute routing in Web API (2), I would like to be able to get route parameters from the URL into the model parameter automatically. The reason for this is that my validation is performed in a filter before it reaches the action, and without this the additional information is not easily available.

Consider the following simplified example:

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

Sample code to post to this:

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

I want the ProductId field in the model to be populated from the route parameters before entering the action method (i.e. so it will be available to my validators).

I'm not sure which part of the model binding process I need to try and override here - I think it's the bit which would handle the [FromBody] part (which is the model parameter in this example).

It is not acceptable to set this within the action itself (e.g. model.ProductId = productId) as I need this to have been set before it reaches the action.

like image 730
Richard Avatar asked Feb 11 '26 23:02

Richard


1 Answers

Referencing this article Parameter Binding in ASP.NET Web API

Model Binders

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

Here is a model binder for UpdateProductModel objects that will try to extract route values and hydrate the model with any matching properties found.

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

Setting the Model Binder

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

Given the following simplified example with the above model and 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();
    }
}

The following integration test was used to confirm required functionality

[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);
            }
        }
    }
}
like image 84
Nkosi Avatar answered Feb 15 '26 12:02

Nkosi



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!