Thursday, March 18, 2010

ASP.NET MVC isn't SOLID (or How to Rely on Abstractions in Views and Controllers)

MVC is a great framework for building web applications, however, there's at least one SOLID principle I feel could be implemented more thoroughly: Dependency Inversion Principle. Dependency Inversion Principle states that "code should depend on abstractions, not concretions." This can be seen in the views, where the generic type parameter of strongly-typed views is typically based on a concrete model:

<%@ Page Title="" Language="C#" MasterPageFile="../Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyNamespace.Widget>" %>

It can also be seen in the controller, where POST actions may have parameters that are concrete:

[HttpPost]
public void Details([Bind] Widget widget)
{
// do something with the widget.
}

Ideally, it'd be nice to rely on an interface for both of these situations, however, it's a little harder than simply replacing Widget with IWidget in the controller and the view. Somehow MVC has to figure out which implementation to use with IWidget. This is where a little creativity and dependency injection can come in handy.

Internally, MVC uses a class called the DefaultModelBinder to reconstitute the strongly-typed view model from the posted form data sent via the POST. This class's "BindModel" method is called from the controller, and the type expected by the controller's action is passed as a property of the ModelBindingContext parameter expected by BindModel. There's a whole slew of things BindModel goes through to bind the form data onto the model, but the only thing we're interested in at the moment is how it creates an instance of an object based on a specific type. Turns out this is accomplished via the "CreateModel" method.

Now, the CreateModel method is very simple. Ultimately, unless the type expected is an IDictionary<T> or an IEnumerable<T>, it's going to simply make a call to Activator.CreateInstance, pass in the type requested, and return the result. Luckily for us, CreateModel is virtual, so we can override it with something like this:

public class DependencyInjectionModelBinder: DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
object result;

// Attempt to get the value from ObjectFactory. If this fails
// use the default method of constructing the model.
if ((result = ObjectFactory.TryGetInstance(modelType)) == null)
result = base.CreateModel(controllerContext, bindingContext, modelType);

return result;
}
}

Next, we need to ensure this new model binder is the default model binder. To do this, we need to simply add this line in our global.asax.cs file, somewhere where it will be called from Application_Start():

ModelBinders.Binders.DefaultBinder = new DependencyInjectionModelBinder();

So, now we have a DependencyInjectionModelBinder registered as our default model binder for our application. Now, supposing that Widget implements IWidget, and you want your view and controller to rely on IWidget instead of Widget, you need to ensure you've registered Widget as the default concrete class of IWidget. Add this line so it will be called from the Application_Start method in the global.asax.cs file:

ObjectFactory.Configure(x => x.For<IWidget>().Use<Widget>());

Lastly, we need to change our view and our controller to use the interface instead of the concrete class:

View:

<%@ Page Title="" Language="C#" MasterPageFile="../Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyNamespace.IWidget>" %>

Controller:

[HttpPost]
public void Details([Bind] IWidget widget)
{
// do something with the widget.
}

That's it! You've now made MVC more SOLID by adhering to dependency inversion principle in your controller and your view. Nowhere in your application, other than when registering Widget as the default implementation of IWidget, should you need to directly reference Widget as a concrete class.