Going further: A Generic ServiceLayer ModelBinder
I wasn't entirely happy with my previous implementation of a generic LINQ-to-SQL ModelBinder. There's a bit of tier-jumping going on, it would be better if the ModelBinder interacted with some loosely-coupled Service Layer. Also, registering my custom ModelBinder in Global.asax messes up the functionality of UpdateModel and TryUpdateModel, both very important tools for handling form posting situations.
With this in mind, I've adapted the concept to apply more generally.
Before I begin, let's revisit my reasons for developing this pattern:
- You generally have to repeat code in your action methods to translate an id into a domain object.
- Whether that code calls a database, O/RM, or a Service Layer, it generally makes your action methods more difficult to unit test.
By pushing SELECT logic into the ModelBinder, many of our controllers become trivially simple to test:-
public ActionResult Details(int id)
{
Product p =
Context.Products.FirstOrDefault(x => x.Id == id);
if ( p == null )
return RedirectToAction("Index");
return View(p);
}
- becomes:-
public ActionResult Details(Product p)
{
if ( p == null )
return RedirectToAction("Index");
return View(p);
}
The former requires a mock DataContext (or a mock Service Layer). The latter does not. You can test your ModelBinder once seperately. This is particularly advantageous when you consider that you will repeat this pattern many times over the course of a single system.
The generic ModelBinder works by chaining together a few simple functions inside the ModelBinder framework:-
- A function to give your ModelBinder access to some source of data - an instance of your DataContext or Service Layer, whether this is being instantiated directly by the ModelBinder, or referenced from elsewhere
- A function to select the correct table (of a DataContext) or function (of a Service Layer)
- A function to retrieve the primary key of the record we want from the request
- A function to query the table, or call the function, with the primary key
As little or as much of that as you require can be delegated out. As many or as few of the types as you want can be generic.
My implementation follows:-
- T is the type of the domain object we want
- K is the type of the domain object's primary key
- IServiceLayer is the interface to our Service Layer
- ControllerBase is our base controller type, which has a reference to an instance of our Service Layer.
public class ServiceLayerBinderBase<K,T> : IModelBinder
{
protected Func<IServiceLayer, K, T> _mappingFunction;
protected String _primaryKeyName;
public object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext
)
{
ControllerBase controller = controllerContext.Controller as ControllerBase;
if ( controller == null )
throw new InvalidOperationException(
"Controller does not derive from ControllerBase");
IServiceLayer service = controller.Service;
if ( service == null )
throw new InvalidOperationException(
"Null Service Layer");
if ( !bindingContext.ValueProvider.ContainsKey(_primaryKeyName) )
return null;
try
{
K primaryKey = (K)bindingContext.ValueProvider[_primaryKeyName]
.ConvertTo(typeof(K));
return _mappingFunction(service, primaryKey);
}
catch ( InvalidCastException )
{
return null;
}
}
#endregion
}
You can then extend that on a per-domain-object basis, and have the child classes set the mapping function and primary key name as appropriate in their no-arg constructor.
Because you can now register the ModelBinder with a no-arg constructor, you now have the option of specifying the ModelBinder inline on your Action methods:-
[HttpPost]
public ActionResult Edit(
[ModelBinder(typeof(Binders.ProductBinder))] Product p,
FormCollection form
)
{
if ( p == null )
return RedirectToAction("Index");
TryUpdateModel(p, whitelist);
...
- which leaves the functionality of UpdateModel and TryUpdateModel intact, allowing you to carry on using those methods to handle form posts.
I've attached a quick demo project demonstrating the separation of concerns aspect of this new ModelBinder, plus how easy it is to unit test Action methods that recieve a domain object instead of a primary key.
I'd love to know what you think. Is it appropriate to be pushing this functionality out of the controller into the ModelBinder? Is it useful?
Until next time.
| File | Size |
|---|---|
| ServiceLayerModelBinder.zip | 1.23 MB |
Post new comment