Tuesday, April 06, 2010

A Custom ASP.NET MVC Model Binder for Repositories

How do you take the values posted by an HTML form and turn them into a populated domain entity? One popular technique is to bind the POST values to a view-model and then map the view-model values to an entity. Since your action method’s argument is the view-model, it allows you to decide in the controller code if the view-model is a new entity or an existing one that should be retrieved from the database. If the view-model represents a new entity you can directly create the entity from the view-model values and then call your repository in order to save it.  In the update case, you can directly call your repository to get a specific entity and then update the entity from the values in the view-model.

However, this method is somewhat tedious for simple cases. Is a view-model always necessary? Wouldn’t it be simpler to have a model binder that simply created the entity for you directly? Here’s my attempt at such a binder:

updated to fix bug when a child entity is changed

public class EntityModelBinder : DefaultModelBinder, IAcceptsAttribute
{
    readonly IRepositoryResolver repositoryResolver;
    EntityBindAttribute declaringAttribute;

    public EntityModelBinder(IRepositoryResolver repositoryResolver)
    {
        this.repositoryResolver = repositoryResolver;
    }

    protected override object CreateModel(
        ControllerContext controllerContext, 
        ModelBindingContext bindingContext, 
        Type modelType)
    {
        if (modelType.IsEntity() && FetchFromRepository)
        {
            var id = GetIdFromValueProvider(bindingContext, modelType);
            if (id.HasValue && id.Value != 0)
            {
                var repository = repositoryResolver.GetRepository(modelType);
                object entity;
                try
                {
                    entity = repository.GetById(id.Value);
                }
                finally
                {
                    repositoryResolver.Release(repository);
                }
                return entity;
            }
        }

        // Fall back to default model creation if the target is not an existing entity
        return base.CreateModel(controllerContext, bindingContext, modelType);
    }

    protected override object GetPropertyValue(
        ControllerContext controllerContext, 
        ModelBindingContext bindingContext, 
        System.ComponentModel.PropertyDescriptor propertyDescriptor, 
        IModelBinder propertyBinder)
    {
        // any child entity property which has been changed, needs to be retrieved by calling CreateModel, 
        // we can force BindComplexModel (in DefaultModelBinder) do this by
        // setting the bindingContext.ModelMetadata.Model to null
        var entity = bindingContext.ModelMetadata.Model as IEntity;
        if (entity != null)
        {
            var id = GetIdFromValueProvider(bindingContext, bindingContext.ModelType);
            if (id.HasValue && id.Value != entity.Id)
            {
                bindingContext.ModelMetadata.Model = null;
            }
        }
        return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
    }


    private static int? GetIdFromValueProvider(ModelBindingContext bindingContext, Type modelType)
    {
        var fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, modelType.GetPrimaryKey().Name);
        if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey))
        {
            return null;
        }

        var result = bindingContext.ValueProvider.GetValue(fullPropertyKey);
        if (result == null) return null;
        var idAsObject = result.ConvertTo(typeof (Int32));
        if (idAsObject == null) return null;
        return (int) idAsObject;
    }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;
        var model = base.BindModel(controllerContext, bindingContext);
        ValidateEntity(bindingContext, controllerContext, model);
        return model;
    }

    protected virtual void ValidateEntity(
        ModelBindingContext bindingContext, 
        ControllerContext controllerContext, 
        object entity)
    {
        // override to provide additional validation.
    }

    private bool FetchFromRepository
    {
        get
        {
            // by default we always fetch any model that implements IEntity
            return declaringAttribute == null ? true : declaringAttribute.Fetch;
        }
    }

    public virtual void Accept(Attribute attribute)
    {
        declaringAttribute = (EntityBindAttribute)attribute;    
    }

    // For unit tests
    public void SetModelBinderDictionary(ModelBinderDictionary modelBinderDictionary)
    {
        Binders = modelBinderDictionary;
    }
}

I’ve simply inherited ASP.NET MVC’s DefaultModelBinder and overriden the CreateModel method. This allows me to check if the type being bound is one of my entities and then grabs its repository and gets it from the database if it is.

Now, I’m most definitely not doing correct Domain Driven Development here despite my use of terms like ‘entity’ and ‘repository’. It’s generally frowned on to have table-row like settable properties and generic repositories. If you want to do DDD, you are much better off only binding view-models to your views.

5 comments:

Anonymous said...

I used to do something like this before but changed my mind when i wanted better validation and client-side logic. I still do it for Show-requests though.

Ie, instead of using Show(int id) i use Show(MyEntity entity). But for Edit/Create scenarios I use a view-model.

Mike Hadlow said...

Anonymous,

Yes, I think it's an interesting debate. As I implied in the post, if you're doing anything complex, you are much better off with the view-model approach.

If I need to do extra validation, I can extend EntityModelBinder and override the ValidateEntity method.

jtaal said...

Hi Mike,

Which version of asp.net mvc is used? How did you configure your model binder?

Thanks Jaap

Mike Hadlow said...

Hi Jaap,

This works with MVC 2.0. Have a look at the Suteki Shop source to see how it's configured:

http://code.google.com/p/sutekishop/source/browse/trunk/Suteki.Shop/Suteki.Shop/Global.asax.cs

and

http://code.google.com/p/sutekishop/source/browse/trunk/Suteki.Shop/Suteki.Shop/IoC/ContainerBuilder.cs

Alex Maines said...

Hello Mike

The problem with the view-model-approach is that developers fill up the codebase with copies of entities, each tailored to each view, diluting the responsibilities represented by the properties on the entities, creating a high risk of shotgun surgery when an entity's interface changes.

So, your model binder seems like the approach that should always be used when presenting a domain entity, saving the view-model-approach for when you want to present a complex or compound model.

Any thoughts?

Alex Maines