Published 16 February 2015

Binding abstract models in ASP.NET MVC

Binding abstract models in ASP.NET MVC

Having trouble binding abstract models in ASP.NET MVC? Hopefully this will help.

The problem

It is common to have abstract models but it can be a bit of struggle to make it work together with MVC’s built in model binder. I guess a lot of people have faced the following yellow screen of death (“Cannot create an abstract class”):

A lot of approaches for this particular issue flourish the web. Such as:

  • For JSON data, using the TypeNameHandling setting to include the .NET fully qualified name of the model
  • One action per concrete model and then let the client route to the specific action
  • A hidden field in the form that describes the .NET fully qualified name of the model
  • Etc.

These approaches feels a bit hacky and cumbersome to me, whereas I’ve created an alternative approach.

An alternative approach

I often tend to include an enum type to my abstract classes that defines the underlying concrete type. Something similar to:

Since the model’s type is present as a value, why not opt-in and guide the DefaultModelBinder what the concrete type of the model is (based on the enum type value provided in the request) and then let the binder continue do its magic.

public class CustomModelBinder<TEnum> : DefaultModelBinder
    where TEnum : struct
{
    private readonly string _key;
    private readonly Dictionary<TEnum, Type> _typeMapper;

    public CustomModelBinder(string key, Dictionary<TEnum, Type> typeMapper)
    {
        _key = key;
        _typeMapper = typeMapper;
    }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        ValueProviderResult value = bindingContext.ValueProvider.GetValue(_key);

        TEnum @enum;
        if (value == null || !Enum.TryParse(value.AttemptedValue, true, out @enum)
            || !_typeMapper.ContainsKey(@enum))
        {
            return base.BindModel(controllerContext, bindingContext);
        }

        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, _typeMapper[@enum]);
        return base.BindModel(controllerContext, bindingContext);
    }
}

The CustomModelBinder hooks into the binding process by overridíng the BindModel method of the DefaultModelBinder. The custom binder updates the binding context’s metadata with the concrete type, based on the type value in the request, and then the DefaultModelBinder continue its work.

The ConcreteModelBinderAttribute is created to decorate in-parameters and serves the purpose to instantiate the correct model binder. One attribute should be created per abstract model.

public class ConcreteModelBinderAttribute : CustomModelBinderAttribute
{
    private readonly string _key;

    public ConcreteModelBinderAttribute(string key = "type")
    {
        _key = key;
    }

    public override IModelBinder GetBinder()
    {
        return new CustomModelBinder(_key, new Dictionary<ConcreteType, Type>
        {
            {ConcreteType.A, typeof (ConcreteA)},
            {ConcreteType.B, typeof (ConcreteB)}
        });
    }
}

 

For the sake of this example, I just added a dictionary that serves as a type map but one could change this to use an IoC-container, ServiceLocator, or similar instead.

Example

So if we create an MVC controller as follows (the in-parameter is decorated with the ConcreteModelBinder described previously):

public class ExampleController : Controller
{
    [HttpPost]
    public ActionResult Index([ConcreteModelBinder]Base model)
    {
        return Json(model);
    }
}

And use the marvellous chrome extension Postman to create a POST request to the server. We get the following result:

The model binder now instantiate the correct concrete class, the only thing we have to remember is to provide the type in the request to the server.

Great success.

Having trouble binding abstract models in ASP.NET MVC? Hopefully this will help.

The problem

It is common to have abstract models but it can be a bit of struggle to make it work together with MVC’s built in model binder. I guess a lot of people have faced the following yellow screen of death (“Cannot create an abstract class”):

A lot of approaches for this particular issue flourish the web. Such as:

  • For JSON data, using the TypeNameHandling setting to include the .NET fully qualified name of the model
  • One action per concrete model and then let the client route to the specific action
  • A hidden field in the form that describes the .NET fully qualified name of the model
  • Etc.

These approaches feels a bit hacky and cumbersome to me, whereas I’ve created an alternative approach.

An alternative approach

I often tend to include an enum type to my abstract classes that defines the underlying concrete type. Something similar to:

Since the model’s type is present as a value, why not opt-in and guide the DefaultModelBinder what the concrete type of the model is (based on the enum type value provided in the request) and then let the binder continue do its magic.

public class CustomModelBinder<TEnum> : DefaultModelBinder
    where TEnum : struct
{
    private readonly string _key;
    private readonly Dictionary<TEnum, Type> _typeMapper;

    public CustomModelBinder(string key, Dictionary<TEnum, Type> typeMapper)
    {
        _key = key;
        _typeMapper = typeMapper;
    }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        ValueProviderResult value = bindingContext.ValueProvider.GetValue(_key);

        TEnum @enum;
        if (value == null || !Enum.TryParse(value.AttemptedValue, true, out @enum)
            || !_typeMapper.ContainsKey(@enum))
        {
            return base.BindModel(controllerContext, bindingContext);
        }

        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, _typeMapper[@enum]);
        return base.BindModel(controllerContext, bindingContext);
    }
}

The CustomModelBinder hooks into the binding process by overridíng the BindModel method of the DefaultModelBinder. The custom binder updates the binding context’s metadata with the concrete type, based on the type value in the request, and then the DefaultModelBinder continue its work.

The ConcreteModelBinderAttribute is created to decorate in-parameters and serves the purpose to instantiate the correct model binder. One attribute should be created per abstract model.

public class ConcreteModelBinderAttribute : CustomModelBinderAttribute
{
    private readonly string _key;

    public ConcreteModelBinderAttribute(string key = "type")
    {
        _key = key;
    }

    public override IModelBinder GetBinder()
    {
        return new CustomModelBinder(_key, new Dictionary<ConcreteType, Type>
        {
            {ConcreteType.A, typeof (ConcreteA)},
            {ConcreteType.B, typeof (ConcreteB)}
        });
    }
}

 

For the sake of this example, I just added a dictionary that serves as a type map but one could change this to use an IoC-container, ServiceLocator, or similar instead.

Example

So if we create an MVC controller as follows (the in-parameter is decorated with the ConcreteModelBinder described previously):

public class ExampleController : Controller
{
    [HttpPost]
    public ActionResult Index([ConcreteModelBinder]Base model)
    {
        return Json(model);
    }
}

And use the marvellous chrome extension Postman to create a POST request to the server. We get the following result:

The model binder now instantiate the correct concrete class, the only thing we have to remember is to provide the type in the request to the server.

Great success.