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.

FileSize
ServiceLayerModelBinder.zip1.23 MB
I like this

This is great code. This isn't what I was looking for, but it's very nice and detailed.

I'd like to see a scenario where the tryupdatemodel is passed down into the service layer because in my project validation occurs in the service layer and not the controller. This simplifies having multiple UIs.

Submitted by Kyle Bailey (not verified) on 23 July, 2010 - 00:38.
TryUpdateModel in service layer

The "modern" way of doing it seems to be via data annotations.

If you need validation that can't be done with the existing data annotations you can roll your own and override its IsValid method.

So rather than passing TryUpdateModel down into the ServiceLayer, you have ValidationAttributes on your models which tell whatever is modifying the model whether or not the model is valid.

Does that satisfy your goal of not adding ModelState errors twice in two different UIs?

Otherwise, I'm sure it is certainly possible to do the TryUpdateModel in the service layer (passing down a wrapper for your model errors dictionary), but you have to do a lot of the plumbing yourself.

Submitted by Iain on 9 November, 2010 - 15:27.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <pre>
  • Lines and paragraphs break automatically.

More information about formatting options