Binding abstract action parameters in WebAPI

痞子三分冷 提交于 2020-01-12 07:28:08

问题


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?


回答1:


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.



来源:https://stackoverflow.com/questions/21306305/binding-abstract-action-parameters-in-webapi

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!