Question

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?

Was it helpful?

Solution

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top