web api model binding to an interface

chrisl91 picture chrisl91 · Aug 20, 2013 · Viewed 14.4k times · Source

I'm trying to bind a controller action to an interface but still maintain the default binding behavior.

public class CoolClass : ISomeInterface
{
    public DoSomething {get;set;} // ISomeInterface
}

public class DosomethingController : ApiController
{
    public HttpResponseMessage Post(ISomeInterface model)
    {
        // do something with model which should be an instance of CoolClass
    }
} 

The consumer of my service knows nothing of CoolClass so having them add "$type" to the Json they are passing would be a hack in my opinion. I'd like to be able to handle it in the service. If I specify CoolClass as the action parameter it works fine.

EDIT: So I found a partial solution to my question here Dependency injection for ASP.NET Web API action method parameters but there is a follow up issue. That solution does not resolve interface properties. See my example below.

IConcreteClass will be resolved, but ISubtype will not.

public class SubConcreteClass : ISubtype
{
    // properties
}

public class ConcreteClass : IConcreteClass
{
    public ISubtype Subtype {get;set;}
}

Once the media formatter sees that is can resolve the type in IConcreteClass, it then reads the entire stream. So I'm guessing there is no chance to resolve interface members.

Answer

chrisl91 picture chrisl91 · Aug 28, 2013

So in case this can help anyone else, I'll go ahead and post the solution I came up with.

As I mentioned above, the interface parameters of the action method can be resolved using DI. But the interface members of that object need to be handled differently.

I created 2 types of Json converters, a single entity type and a collection type, to decorate the interface properties.

Here's a class that needs to be resolved as an action interface parameter.

public class CreateEnvelopeModel : ICreateEnvelopeCommand
{
    [JsonConverter(typeof(EntityModelConverter<CreateEmailModel, ICreateEmailCommand>))]
    public ICreateEmailCommand Email { get; set; }
    [JsonConverter(typeof(CollectionEntityConverter<CreateFormModel, ICreateFormCommand>))]
    public IList<ICreateFormCommand> Forms { get; set; }
}

Here's the controller action method

public HttpResponseMessage PostEnvelope(ICreateEnvelopeCommand model)
{
    // do stuff
}

Here's the 2 json converters

public class EntityModelConverter<T, Tt> : JsonConverter where T : Tt
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(Tt));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        return serializer.Deserialize<T>(reader);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value, typeof(T));
    }
}

public class CollectionEntityConverter<T, Tt> : JsonConverter where T : Tt
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(IList<Tt>));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        IList<Tt> items = serializer.Deserialize<List<T>>(reader).Cast<Tt>().ToList();
        return items;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value, typeof(IList<T>));
    }
}