Since I started to use ASP.NET MVC, I enjoy model binders. They take a huge amount of irrelevant infrastructure code out sight, out of code, and out of mind, making it easier to create and maintain.
Now, one of the most helpful custom binders that I use if the one that binds entity by id.
The basic idea is that if we have
public ActionResult Action(Entity entity) {}
or in view model
public Entity EntityProperty { get; set; }
we don't have to write any code - entity will be bound to data by the binder, and we get either object and no errors, or null and failed ModelState.
public ActionResult Action(Entity entity)
{
if (!ModelState.IsValid) { return ... }
}
Here we have model state errors for all failed entities (invalid IDs from page, not found in DB, etc), without any single line of code (except our one-for-all binder).
In the views, I have something like
${Html.HiddenFor(x => x.EntityProperty)}
I can't use .EditorFor(x => x.EntityProperty.Id) because I will get wrong name in POST, so how do I get id in the input value? There're two approaches:
1. Tweak view template or html helpers, so that, when entity (derived from BaseEntity, for example) is passed to HiddenFor, it uses .Id instead of .ToString()
2. Override Entity.ToString() to return .Id.ToString()
Now, even if we do Html.EditorFor(Model) - for entire model - we get proper IDs for entities, and we get them back on POST - without any additional code at all.
Sometimes, we want to bind by another property, not Id. E.g. in REST/urls we want product name as parameters, not ids - because it looks better in URLs. Hard? Not at all, we just apply a different binder:
public ActionResult Action([ModelBinder(typeof(EntityNameBinder))] Entity entity) {}
where our EntityNameBinder is:
public class EntityNameBinder : EntityBinder
{
public EntityNameBinder() : base("Name") { }
}
- that is, we just pass property name to bind to. Internally binder uses repository to find entities (simplified):
var rep = ServiceLocator.Current.GetInstance(typeof(IBaseRepository<>).MakeGenericType(entityType));
var boundValue = rep.GetType().GetMethod("FindOne").Invoke(rep, new object[]{ new Dictionary<string, object>{{propertyName, valueFromView}} });
This method works even for lists, we just tweak our custom binder a bit to detect IList<Entity>/Entity. And so we can use this in view models:
public IList<Entity> EntityListProperty { get; set; }
once again without any single line of additional code.
Another usefull aspect of this approach, is that controllers code is very clean - model in, model view out - and thus very easy to test.
And, last but not least, one should never bind to entities directly - because it is not safe (remote user can bind to non-desired public properties, etc). With this custom binder, this is completely eliminated - there's no way to fill entity properties with values from the page, period. The workflow is always to create view model (whose properties get values from the page) and then use it populate entity (using AutoMapper, for example).