A better Model Binder
One of the more interesting extension points in ASP.NET MVC are the Model Binders. Model Binders are tasked with transforming the HTTP Form and Querystring information and coercing real .NET types out of them. A normal POST is merely a set of string key-value pairs, which isn’t that fun to work with.
Back in the ASP 3.0 days, where I cut my teeth, we did a lot of “Request.Form(“CustFirstName”)” action, and just dealing with the mapping from HTTP to strong types manually. That wasn’t very fun.
ASP.NET MVC supplies the DefaultModelBinder, which is able to translate HTTP request information into complex models, including nested types and arrays. It does this through a naming convention, which is supported at both the HTML generation (HTML helpers) and the consumption side (model binders).
Reconstituting complex model objects works great, especially in form posting scenarios. But there are some scenarios where we want more complex binding, and for these scenarios, we can supply custom model binders.
Quick glance at custom model binders
ASP.NET MVC allows us to override both the default model binder, as well as add custom model binders, through the ModelBinders static class:
ModelBinders.Binders.DefaultBinder = new SomeCustomDefaultBinder(); ModelBinders.Binders.Add(typeof(DateTime), new DateTimeModelBinder());
The custom binders are bound by destination type. We can also use the Bind attribute to supply the type of the custom binder directly on our complex model type. The resolution code (from CodePlex) is:
private IModelBinder GetBinder(Type modelType, IModelBinder fallbackBinder) { // Try to look up a binder for this type. We use this order of precedence: // 1. Binder registered in the global table // 2. Binder attribute defined on the type // 3. Supplied fallback binder IModelBinder binder; if (_innerDictionary.TryGetValue(modelType, out binder)) { return binder; } binder = ModelBinders.GetBinderFromAttributes(modelType, () => String.Format(CultureInfo.CurrentUICulture, MvcResources.ModelBinderDictionary_MultipleAttributes, modelType.FullName)); return binder ?? fallbackBinder; }
Very nice, they even commented the order of precedence!
This is all well and good, but the custom model binding resolution leaves me wanting. It only works if the destination type matches exactly with a type registered in the custom binder collection. That’s great, unless you want to use something like binding against a base class, such as a layer super type of Entity or similar. Suppose I want to bind all Entities by fetching them out of a Repository automatically? That would allow me to just put our entity types as action parameter types, instead of GUIDs littered all over the place. It gets even worse as some ORMs (NHibernate being one) use proxies to do lazy loading, and the runtime type is some crazy derived type you’ve never heard of.
But we can do better.
A new default binder
Instead of using the ModelBinders.Binders.Add method, let’s put all of our matching in our default binder, a new binder, a SmartBinder. This SmartBinder can still have all the default binding logic, but this time, it will attempt to match individual custom binders themselves. To do this, let’s define a new IModelBinder interface:
public interface IFilteredModelBinder : IModelBinder { bool IsMatch(Type modelType); }
Instead of our ModelBinderDictionary containing the matching logic, let’s put this where I believe it belongs – with each individual binder. Model binders make a ton of assumptions about what the model information posted looks like, it makes sense that it’s their decision on whether or not they can handle the model type trying to be bound. We created a new IFilteredModelBinder type, inheriting from IModelBinder, and added an IsMatch method that individual binders can use to match up their type.
The new SmartBinder becomes very simple:
public class SmartBinder : DefaultModelBinder { private readonly IFilteredModelBinder[] _filteredModelBinders; public SmartBinder(IFilteredModelBinder[] filteredModelBinders) { _filteredModelBinders = filteredModelBinders; } public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { foreach (var filteredModelBinder in _filteredModelBinders) { if (filteredModelBinder.IsMatch(bindingContext.ModelType)) { return filteredModelBinder.BindModel(controllerContext, bindingContext); } } return base.BindModel(controllerContext, bindingContext); } }
Our new SmartBinder depends on an array of IFilteredModelBinders. This array is configured by our IoC container of choice (StructureMap in my case), so our SmartBinder doesn’t need to concern itself on how to find the right IFilteredModelBinders.
The BindModel method is overridden, and simply loops through the configured filtered model binders, asking each of them if they are a match to the model type attempting to be resolved. If one is a match, its BindModel method is executed and the value returned.
If there are no matches, the logic defaults back to the built-in DefaultModelBinder progression, going through the commented steps laid out before.
I’ve found that our custom model binders can decide to bind a certain model for quite a variety of reasons, whether it’s a specific layer supertype and the types are assignable, or maybe the type is decorated with a certain attribute, it’s really up to each individual filtered model binder.
That’s the strength of this design – it puts the decision of what to bind into each individual model binder, providing as much flexibility as we need, instead of relying specifically on only one matching strategy, on concrete type. Now testing custom model binders – that’s another story altogether.