Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Binding abstract action parameters in WebAPI

I'm in a situation where I need to bind an incoming HTTP POST request with data in the body, to a concrete type depending on a ProductType denominator in the data. Here is my Web API 2 action method:

[HttpPost, Route]
public HttpResponseMessage New(ProductBase product)
{
    // Access concrete product class...

    if (product is ConcreteProduct)
        // Do something
    else if (product is OtherConcreteProduct)
        // Do something else
}

I was first thinking of using a custom model binder, but it seems like it isn't possible to access the request body at that point:

For complex types, Web API tries to read the value from the message body, using a media-type formatter.

I can't really see how media-type formatters solves this problem, but I'm probably missing something. How would you solve this problem?

like image 790
Tommy Jakobsen Avatar asked Jan 23 '14 11:01

Tommy Jakobsen


People also ask

How do we do parameter binding in Web API?

The item parameter is a complex type, so Web API uses a media-type formatter to read the value from the request body. To get a value from the URI, Web API looks in the route data and the URI query string. The route data is populated when the routing system parses the URI and matches it to a route.

What is parameter binding in ASP.NET Web API?

Binding is a process to set values for the parameters when Web API calls a controller action method. In this article, we learn how to map Web API methods with the different types of the parameters and how to customize the binding process. Web API tries to get the value from the URI.

What is model binding in Web API?

Model Binding is the most powerful mechanism in Web API 2. It enables the response to receive data as per requester choice. i.e. it may be from the URL either form of Query String or Route data OR even from Request Body. It's just the requester has to decorate the action method with [FromUri] and [FromBody] as desired.


1 Answers

Depending on the request content type you will have to decide which concrete class to instantiate. Let's take an example with application/json. For this content type out-of-the-box the Web API is using the JSON.NET framework to deserialize the request body payload into a concrete object.

So you will have to hook into this framework in order to achieve the desired functionality. A good extension point in this framework is writing a custom JsonConverter. Let's suppose that you have the following classes:

public abstract class ProductBase
{
    public string ProductType { get; set; }
}

public class ConcreteProduct1 : ProductBase
{
    public string Foo { get; set; }
}

public class ConcreteProduct2 : ProductBase
{
    public string Bar { get; set; }
}

and the following action:

public HttpResponseMessage Post(ProductBase product)
{
    return Request.CreateResponse(HttpStatusCode.OK, product);
}

Let's write a custom converter to handle this type:

public class PolymorphicProductConverter: JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(ProductBase);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var obj = JObject.Load(reader);
        ProductBase product;
        var pt = obj["productType"];
        if (pt == null)
        {
            throw new ArgumentException("Missing productType", "productType");
        }

        string productType = pt.Value<string>();
        if (productType == "concrete1")
        {
            product = new ConcreteProduct1();
        }
        else if (productType == "concrete2")
        {
            product = new ConcreteProduct2();
        }
        else
        {
            throw new NotSupportedException("Unknown product type: " + productType);
        }

        serializer.Populate(obj.CreateReader(), product);
        return product;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

and the last step is to register this custom converter in the WebApiConfig:

config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(
    new PolymorphicProductConverter()
);

And that's pretty much it. Now you can send the following request:

POST /api/products HTTP/1.1
Content-Type: application/json
Host: localhost:8816
Content-Length: 39

{"productType":"concrete2","bar":"baz"}

and the server will properly deserialize this message and respond with:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
Date: Sat, 25 Jan 2014 12:39:21 GMT
Content-Length: 39

{"Bar":"baz","ProductType":"concrete2"}

If you need to handle other formats such as application/xml you might do the same and plug into the corresponding serializer.

like image 66
Darin Dimitrov Avatar answered Sep 29 '22 10:09

Darin Dimitrov