Using abstract classes as controller parameters in ASP.NET MVC4

As a part of the API for our product, we have the need to allow the UI to send us a parcel of data, and we need to process it differently based solely on a single parameter in the data.  We have decided to create an abstract model class to handle this, but MVC's DefaultModelBinder doesn't work out of the box on abstract classes (or interfaces).  There are many discussions out there on this, and the accepted way to solve this problem is to create a new ModelBinder class that has more knowledge of the internals of your application than the default does.  I thought about this some, and I decided to try out a few tweaks to the process.

First of all, I wanted to have a clean POCO for interaction with the user interface; no functionality, just a property bag.  This is a common pattern, and it works well in our project.  Most of our controllers are implemented so that they take the POCO directly, and objects are constructed using a factory injected into the controller.  This works, but I feel that it adds some unnecessary noise into the controller code, and I set out to create a custom ModelBinder to clean this up.

First, I needed to ensure that any POCO created would have some property that identifies the underlying type that needs to be constructed.  In this simple implementation, I simply created an interface that all of my POCOs would implement:

public interface IPoco
{
    string Type { get; }
}

Of course, this is somewhat over-simplified, and I intend to expand this in the future, but for purposes of this discussion, it will suffice.

I also need some way to initialize a model with the data contained in the POCO.  I created an interface for the model classes also:

public interface IModel
{
    void InitializeFromPoco(IPoco poco);
}

And a default implementation (using AutoMapper):

public abstract class ModelBase : IModel
{
    public virtual void InitializeFromPoco(IPoco poco)
    {
        Mapper.DynamicMap(poco, this, poco.GetType(), GetType());
    }
}

Almost there, but I still need a factory of some sort:

public interface IModelResolver
{
    IModel ResolveModel(Type modelType, IPoco poco);
}

There is one more piece that I needed to put in place before I could write the custom `ModelBinder`.  I needed a way to link an abstract model to its POCO.  I decided the simplest way was with a custom attribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
public class PocoAttribute : Attribute
{
    public Type PocoType { get; private set; }

    public PocoAttribute(Type pocoType)
    {
        PocoType = pocoType;
    }
}

And finally, the ModelBinder itself:

public class PocoModelBinder : DefaultModelBinder
{
    private readonly IModelResolver _modelResolver;

    public PocoModelBinder(IModelResolver modelResolver)
    {
        // Work around a bug in msbuild that causes the AutoMapper.Net4 assembly to be missed.
        var workAround = new ListSourceMapper();
        _modelResolver = modelResolver;
    }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var pocoAttribute = bindingContext.ModelType.GetCustomAttributes(typeof(PocoAttribute), true).FirstOrDefault()
as PocoAttribute; if (pocoAttribute == null) return base.BindModel(controllerContext, bindingContext); var bc = new ModelBindingContext(bindingContext); var modelMetaData = new ModelMetadata( ModelMetadataProviders.Current, null, null, pocoAttribute.PocoType, null); bc.ModelMetadata = modelMetaData; var poco = base.BindModel(controllerContext, bc) as IPoco; if (poco == null) return base.BindModel(controllerContext, bindingContext); IModel model = _modelResolver.ResolveModel(bindingContext.ModelType, poco); model.InitializeFromPoco(poco); return model; } }

What is happening here?  First, in our constructor, we accept an instance of IModelResolver that is defined by the application.  Note in line 8, an unused variable.  This is simply there because AutoMapper installs a second assembly, AutoMapper.Net4, that is implicitly referenced, and (if this code is in a library) msbuild will fail to copy it into the web application's bin directory without something explicitly referencing it.

The bulk of the work is done in the BindModel implementation.  First we check to see if the model type has the PocoAttribute on it.  If not, we let the default implementation do the work.

There's a little bit of MVC magic in lines 18-25.  Our goal is to trick the default ModelBinder to create and populate an object of our poco type.  To do this, we need to provide a new ModelBindingContext to the base implementation, explicitly specifying the poco type.  If we get a type that implements IPoco, then we continue, otherwise we let the default implementation do its work.

Finally, we call our instance of IModelResolver to get the concrete implementation, and copy the data in the poco into the object.  And that's all there is to it.

Putting it together

Let's create a POCO, an abstract model, and a couple of concrete models that make use of this framework:

public class TestViewModel : IPoco
{
    public string Name { get; set; }
    public string Type { get; set; }
}

[Poco(typeof(TestViewModel))]
public abstract class TestModel : ModelBase
{
    public string Name { get; set; }

    public abstract string GetString();
}

public class TestModelA : TestModel
{
    public override string GetString()
    {
        return String.Format("Hello {0}, this is Model A", Name);
    }
}

public class TestModelB : TestModel
{
    public override string GetString()
    {
        return String.Format("Hello {0}, this is Model B", Name);
    }
}

The TestViewModel class is our POCO, and it is the contract that the user interface can follow. Inside of TestModel, we have a simple abstract method (GetString) who's behavior will differ depending on the concrete type ultimately constructed (TestModelA or TestModelB). Note that there is no "Type" property, as that is no longer necessary. Each concrete type will know what it needs to do.

A simple controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new TestModelA());
    }

    [HttpPost]
    public ActionResult Index(TestModel model)
    {
        return View(model);
    }

    [HttpPost]
    public ActionResult TheOldWay(TestViewModel viewModel)
    {
        var someModelFactory = IocContainer.Get(); // Or use constructor injection
        TestModel model = someModelFactory.GetFromViewModel(viewModel);
 
        return View(model);
    }
}

The default Index() method in lines 3-6 just sets up a default view. The second method in lines 8-12 is the one that invokes our new model binder. Note that it accepts the base class we defined above, and not the view model. An example of how we could have done things without our custom model binder is in the method TheOldWay, lines 14-20. Note that this requires that we have a factory of some sort and an explicit conversion from the view model to the model.

The following view needs no real explanation, I am including it for completeness:

@model TestModel

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
    <head>
        <title>Test</title>
    </head>
    <body>
        <div>
            <h1>@Model.GetString()</h1>
            @using (Html.BeginForm())
            {
                @Html.EditorForModel()
                @Html.DropDownList("Type", new [] {new SelectListItem(){Text = "TypeA", Value = "TypeA"}, new SelectListItem() {Text = "TypeB", Value="TypeB"}  })
                <input type="submit"/>
            }
        </div>
    </body>
</html>

We are almost there, but we still need to tell the MVC pipeline to use the new model binder.  First, we implement the binder's dependency, IModelResolver:

public class ModelResolver : IModelResolver
{
    public IModel ResolveModel(Type modelType, IPoco poco)
    {
        if (poco.Type == "TypeA") return new TestModelA();
        return new TestModelB();
    }
}

This implementation very simply chooses between TestModelA and TestModelB based on the value of the Type property in the view model. A more robust and generic implementation might use an IoC container that has named instances set up, for example, or reflection techniques could be used. The great part about this kind of pattern is that it is possible to implement just the right level of complexity for your application's needs.

Finally, with an IModelResolver implemented, we can set the DefaultBinder in Global.asax.cs:

protected void Application_Start()
{
    // Other setup stuff...
    ModelBinders.Binders.DefaultBinder = new PocoModelBinder(new ModelResolver());
}

Using this custom model binder as a framework, you can clean up your controller code, and make the intent of your public API more clear. It can also clean up your models and help you to keep your code adherent to the SOLID principles, especially the Single Responsibility Principle and the Liskov Substitution Principle.

Note: Although this article was written for the MVC4 framework, a similar approach can be used in the ASP.NET WebAPI. Look for a future post with those details.

No Comments

Add a Comment