StructureMap, Model Binders and Dependency Injection in ASP.NET MVC 3

In our last post we setup the basic dependency resolver infrastructure for StructureMap. Our sample code is still hosted up on Github, so go get if you want to take a look at it. The same caveat to the last discussion still applies: since I know of no samples where StructureMap is used for a full implementation of the IoC hooks in ASP.NET MVC 3, I am kinda making it up as I go along. It is certainly working but time will tell how close my implementation ends up to being awesome. Now that this is out of the way, we need to push on because as is we are in little shape than we were in ASP.NET MVC 2 in terms of IoC support. So let’s start with getting dependency injection going for model binders.

IModelBinderProvider

To setup DI for model binders you implement the IModelBinderProvider interface. The interface has one method, GetBinder(Type), and when it is called you are supposed to return a type of IModelBinder, presumably with its dependencies injected. We’ll start with our basic class.


public class StructureMapModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(Type modelType)
    {
        return null;
    }
}

Here is the setup in the global asax, still including what we put in there last time. In this case the only new and interesting bit is “x.For<IModelBinderProvider>().Use<StructureMapModelBinderProvider>();”.


protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
    IContainer container = new Container(x =>
    {
        x.For<IControllerActivator>().Use<StructureMapControllerActivator>();
        x.For<IModelBinderProvider>().Use<StructureMapModelBinderProvider>();
        x.For<IBar>().Use<Bar>();
    });
    DependencyResolver.SetResolver(new StructureMapDependencyResolver(container));
}    

And just so we have something we want to mess around with, we’ll create a class and a model binder that we’ll attempt to finish implementing and hook up.


public class AThingModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        return base.CreateModel(controllerContext, bindingContext, modelType);
    }
}
public class AThing
{
    public string SomeRandomValue { get; set; }
}

Implementation

So we have our basic but useless implementation of IModelBinderProvider. It still needs to perform two things: first, it needs to get the model binders for specified types; second, it needs to dependency inject them.

Now for the first task. In v1 and v2 of ASP.NET MVC, you registered model binders like this in your global asax:


ModelBinders.Binders.Add(typeof(AThing), new AThingModelBinder());

That worked I suppose but it doesn’t give us a very rich dependency inversion path. In the new way of doing things, we need to somehow let our new provider know about all the types it will need to support (without using ModelBinders.Binders, of course). What I have seen on how to do this is very fuzzy, so what I’m going to do now is completely made up by me and if you have better ideas, please pass them on.

So here is what I’m going to do: the provider still needs some sort of mapping between the type to be bound and the modelbinder that is responsible for doing that. It would have been awesome if I could have used the built-in ModelBinderDictionary, but with that type you register Type and IModelBinder instance, not Type and Type as I would like. So I’m going to create my own dictionary type like so…


public class ModelBinderTypeMappingDictionary : Dictionary<Type, Type>
{
}

Cool. Now I’m going to instantiate it, set up my mapping in my global asax and register it with StructureMap.


protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
    var modelBinderTypeMappingDictionary = new ModelBinderTypeMappingDictionary();
    modelBinderTypeMappingDictionary.Add(typeof(AThing), typeof(AThingModelBinder));
    IContainer container = new Container(x =>
    {
        x.For<IControllerActivator>().Use<StructureMapControllerActivator>();
        x.For<IModelBinderProvider>().Use<StructureMapModelBinderProvider>();
        x.For<ModelBinderTypeMappingDictionary>().Use(modelBinderTypeMappingDictionary);
        x.For<IBar>().Use<Bar>();
    });
    DependencyResolver.SetResolver(new StructureMapDependencyResolver(container));
}

Now we can turn our attention to the implementation of the actual ModelBinderProvider. At this point the implementation is pretty straightforward. First, get the ModelBinderTypeMappingDictionary and check for a type and, if a model binder is registered, use that type to call StructureMap. It might look something like this.


public class StructureMapModelBinderProvider : IModelBinderProvider
{
    public StructureMapModelBinderProvider(IContainer container)
    {
        _container = container;
    }
    private IContainer _container;
    public IModelBinder GetBinder(Type modelType)
    {
        var typeMappings = _container.GetInstance<ModelBinderTypeMappingDictionary>();
        if (typeMappings != null && typeMappings.ContainsKey(modelType))
            return _container.GetInstance(typeMappings[modelType]) as IModelBinder;
        return null;
    }
}

And now we should be able to use constructor injection, courtesy of StructureMap, with our modelbinder. In the example below we aren’t doing much with it but you can obviously do whatever you want with whatever dependencies you want to send in.


public class AThingModelBinder : DefaultModelBinder
{
    public AThingModelBinder(IBar bar)
    {
        _bar = bar;
    }
    private IBar _bar;
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        return new AThing() 
        { 
            SomeRandomValue = _bar.IPityTheFoo() 
        };
    }
}

And done. To register further types for model binding, all you have to do is put them in the type dictionary in the global.asax (or better, put that logic elsewhere in something that the global.asax calls). Hopefully this works for you and, of course, hopefully I am not completely off base. Let me know if you have any questions or critiques.

Comments

Charles Vallance (Saturday, March 5, 2011 5:15 PM)

You could decouple your ModelBinderProvider from StructureMap and simply use DependencyResolver.Current.

I took your idea of using a dictionary and just put that into my IModelBinderProvider.

I like the idea of using an attribute on IModelBinder classes to define what class/model it should be used with. Then use this to register your custom modelbinders. You can see an implementation of that kind of approach using AutoFac here: http://alexmg.com/post/2010/12/07/Model-Binder-Injection-in-Autofac-ASPNET-MVC-3-Integration.aspx

Anyway, here's my current ModelBinderProvider implementation:


public class ModelBinderProvider : IModelBinderProvider
{
    private static IDictionary _modelBinderTypeMappings = new Dictionary
        {                                                                                
            { typeof(AThing), typeof(AThingModelBinder) }                                                                            
        };
    public IModelBinder GetBinder(Type modelType)
    {        
        if (_modelBinderTypeMappings.ContainsKey(modelType))
        {
            return DependencyResolver.Current.GetService(_modelBinderTypeMappings[modelType]) as IModelBinder;       
        }
        return null;    
    }
}

Ps. Keep up the posts, they're definitely helping me fill a few gaps here and there :-)

Eric Sowell (Tuesday, March 8, 2011 7:41 AM)

Decoupling from SM to go back to the DependencyResolver makes a lot of sense. I'll have to update the post with that.



So the scanning idea is an interesting one. I'll have to ponder that one. Thanks for the link and the comments. I appreciate no working in the dark here :)