Intelligent model binding with model binder providers
So that better model binder I built a couple of years ago to address conditional model binding in ASP.NET MVC 1-2 is obsolete with the release of ASP.NET MVC 3. Instead, the concept of a model binder provider allows this same functionality, fairly easily. Back in my Put Your Controllers on a Diet talk at MVCConf, I showed how we can get rid of all those pesky “GetEntityById” calls out of our controller actions. We wanted to turn this:
public ActionResult Show(Guid id) { var conf = _repository.GetById(id); return AutoMapView<ConferenceShowModel>(View(conf)); }
Into this:
public ActionResult Show(Conference conf) { return AutoMapView<ConferenceShowModel>(View(conf)); }
We can use a custom model binder to achieve this result. However, our original implementation used model binders per concrete entity type, not too efficient:
ModelBinders.Binders .Add(typeof(Conference), new ConferenceModelBinder());
The problem here is that we’d have to add a model binder for each concrete entity type. In the old solution from my post for a better model binder, we solved this problem with a model binder that also included a condition on whether or not the model binder applies:
public interface IFilteredModelBinder : IModelBinder { bool IsMatch(ModelBindingContext bindingContext); }
However, this is exactly what model binder providers can do for us. Let’s ditch the filtered model binder and go for the model binder provider route instead.
Custom model binder provider
Before we get to the model binder provider, let’s first build out a generic model binder. We want this model binder to not just accept a single concrete Entity type, but any Entity type we supply:
public class EntityModelBinder<TEntity> : IModelBinder where TEntity : Entity { private readonly IRepository<TEntity> _repository; public EntityModelBinder(IRepository<TEntity> repository) { _repository = repository; } public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { ValueProviderResult value = bindingContext .ValueProvider.GetValue(bindingContext.ModelName); var id = Guid.Parse(value.AttemptedValue); var entity = _repository.GetById(id); return entity; } }
We took the model binder used from the original “controllers on a diet” talk, and extended it to be able to handle any kind of entity. In the example above, all entities derive from a common base class, “Entity”. Our entity repository implementation (although it could be any common data access gateway) allows us to retrieve the specific kind of entity (Customer, Order, Conference, whatever) by filling in the type. It’s just another example of using type information as a means of altering behavior in our system.
In our previous incarnation, we would create either our IFilteredModelBinder to be able to handle any base entity type. If we didn’t have that kind of abstraction in place, we’d have to register concrete implementations for each concrete entity type. Not ideal. Instead, let’s build a model binder provider:
public class EntityModelBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(Type modelType) { if (!typeof(Entity).IsAssignableFrom(modelType)) return null; Type modelBinderType = typeof(EntityModelBinder<>) .MakeGenericType(modelType); var modelBinder = ObjectFactory.GetInstance(modelBinderType); return (IModelBinder) modelBinder; } }
First, we create a class implementing IModelBinderProvider. This interface has one member, “GetBinder”, and the parameter is the type of the model attempting to be bound. Model binder providers return a model binder instance if it’s able to bind based on the model type, and null otherwise. That allows the IModelBinderProviders to have the same function in our IFilteredModelBinder, but just in a slightly modified form.
If it does match our condition, namely that the model type is derived from Entity, we can then use some generics magic to build up the closed generic type of the EntityModelBinder, build it from our IoC container of choice, and return that as our model binder.
Finally, we need to actually register the custom model binder provider:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); ModelBinderProviders.BinderProviders .Add(new EntityModelBinderProvider()); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); }
We now have just one model binder provider that can handle any bound entity in our incoming model in our controller actions. Whereas MVC 1-2 forced us to either come up with a new abstraction or register specific types, the IModelBinderProvider allows us to make intelligent decisions on what to bind, without incurring a lot of duplication costs.
Yet another set of code to delete when moving to ASP.NET MVC 3!