Ian Richards Ian Richards - 6 months ago 415
JSON Question

Web Api Model Binding and Polymorphic Inheritence

I am asking if anyone knows if it is possible to to pass into a Web Api a concrete class that inherits from a abstract class.

For example:

public abstract class A{
A();
}

public class B : A{

}

[POST("api/Request/{a}")]
public class Request(A a)
{
}


At present I have looked around and most solutions seem to say that using TypeNameHandling will work.

JsonMediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
jsonFormatter.SerializerSettings.TypeNameHandling = TypeNameHandling.Auto;


However this is not that case. Also my model is being passed from a console app to the webapi. I have read that I may be able to deserialize the json object and after attempting this a few times I decide this was not going to work.

I have looked into creating a customer model binder however, I do not want to make my application more complex that it has to be. At present I inherit from the abstract class with 3 models but may in the future extend this. As you may note adding custom model binders may require multiple binders unless there is a way of making one binder generic for all types of the abstract class.

To expand on this in my console app I have instantiated class b as such and then passed it to the ObjectContent before posting to my webapi

item = B();

//serialize and post to web api
MediaTypeFormatter formatter;
JsonMediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
jsonFormatter.SerializerSettings.TypeNameHandling = TypeNameHandling.Auto;
formatter = jsonFormatter;

_content = new ObjectContent<A>(item, formatter);
var response = _client.PostAsync("api/Request", _content).Result;


when the webapi action is called the object is null

Answer

UPDATE: As user @Keith pointed out, this solution only applies to MVC, not WebApi. A WebApi solution would implement IModelBinder and follow a similar approach. I will update this answer to be correct for WebApi

I may be almost 2 years late on this, but this question is still unanswered and still fairly high up on the C# unanswered queue. It actually is possible to create one binder that's suitable for all subtypes of the abstract class (that leverages the all the built in functionality of the default model binding behavior) using a bit of reflection. The caveat is that you have to embed enough information to determine the concrete type in the incoming data:

public class AModelBinder : DefaultModelBinder
{
    protected override object CreateModel(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext,
        Type modelType)
    {
        // look at some value in incoming data to decide if this is B:A or C:A
        // in my example I just used a "ModelType" field in my JSON and defined a corresponding property in my abstraction 
        // for security reasons you're probably better off resolving to the string fully qualified type name by switching off some other value
        // you can also use the presence or absence of certain fields within the data as a flag to determine which concrete type you're dealing with
        var dataConcreteTypeAsString = bindingContext.ValueProvider.GetValue("ModelType");
        // dataConcreteTypeAsString == "Namespace.B"

        // get the concrete type by fully qualified name
        var dataConcreteType= Type.GetType(dataConcreteTypeAsString.AttemptedValue, throwOnError: true);

        // make sure that the concrete type represented by the data can be assigned to the abstraction
        if (!modelType.IsAssignableFrom(dataConcreteType))
        {
            throw new InvalidOperationException("Bad Type. Does not inherit from A");
        }

        // create an instance of the concrete type (must have parameterless constructor)
        var model = Activator.CreateInstance(dataConcreteType);

        // let the default binder do all the binding work to the concrete type
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, dataConcreteType);
        return model;
    }
}

Once you've defined the custom binder, register it in App_Start:

ModelBinders.Binders.Add(typeof(A), new AModelBinder());

Then use it in your controller:

public ActionResult Request([ModelBinder(typeof(AModelBinder))] A model)

The bottom half of this MSDN article goes into the topic further and provides some alternative methods (such as route parameters) of getting the FQN of the concrete type to the custom binder.

Comments