Geeks With Blogs
Boy Meets 'Hello World' Blogging the journey from College Grad to .NET Developer

Recently, after some analyzing some of the controllers I was building using ASP.NET MVC, I found that my controllers typically have the same pattern. They all have some dependencies injected into them through the constructor, each action calls a method on one of the dependencies (typically using the arguments of the  and gets a result back), and depending on the result, an ActionResult is returned.

Now, writing the tests wasn't tough. But, I dunno, I guess I'm REALLY lazy. So I started looking today at some sort of an idea for a DSL that could make testing even easier than using all the extension methods found in MVCContrib.

Eventually, I came up with a syntax that was pretty simple, and thought, "Well, I've gone this far, I wonder if I can take it one step further."

I haven't implemented it completely, but the idea is rather than build a class to act as your controller, you build actions using an action builder. The action consist of these things...

1.) A name (typically in the "/area/controller/name", not still sure about how the areas would work under ASP.NET MVC, I don't know if I still grok the whole routing thing)

2.) A lambda expression with one strongly typed arg that represents the parameters of the request.

3.) A key-value of conditions, with the key being a Type, and the value being the action result.

Here is some example code. No implementation, just throwing it out there...

 

   1:  var action = Create.Action("Customer/Orders/ViewPending",
   2:          (CustomerID id) => new ReviewPendingCustomerOrdersTasks(id))
   3:      .Results<PendingCustomerOrdersReview>(RenderView.View())
   4:      .Results<CustomerNotFoundResults>(Redirect.To("NotFound"));
 
 

In the first line, we create the action with the name. The second line has the arguments that we'll bind the parameters into (a CustomerID object), that is used in a lambda expression to say what actual task object should be invoked for this action (more on this below). The argument's type could be as simple as an int or Guid, or as complex as we want our DataBinder to be.

ReviewPendingCustomerOrdersTask is a class that would be in our service layer. Typically, you have things such as ICustomerOrders, which has methods such as GetOrder(id), GetAllOrders(), GetPendingOrders(), etc., and then a class which implements all those methods in your service layer. However, I typically enjoy having as small a number of methods as possible on a class, even if it means more classes, so I would have a GetOrderTask class, a GetAllOrders class, and a GetAllPendingOrders class (I'm also trying to stop using "Get____", and instead come up with terminology for what this task is actually used for). Typically, I would need to then put each of these various services into my IoC container, but since I'm just creating the objects directly, I don't really need the container for these services.

This, of course, assumes that the container will have the ability to use setter-injection on my service that is not included in the container, for dependencies such as the repository. Something like...

   1:  var ioc = MyGreatIoCContainerAlreadyLoadedUpWithStuff;
   2:  var task = new ReviewPendingOrdersTask(args);
   3:  ioc.ResolveOn(task);
 

The ResolveOn method should be able to see that the ReviewPendingOrdersTask object has a public setter for IRepository, and inject the IRepository into it. I'm sure some IOC containers can do this, but I don't know the methods off the top of my head (nor after a quick google search...)

Ok, so now let's take a look at what the implementation of ReviewPendingOrdersTask might actually look like....

   1:  public class ReviewPendingCustomerOrdersTasks : Task
   2:  {
   3:      private readonly CustomerID id;
   4:   
   5:      public ReviewPendingCustomerOrdersTasks(CustomerID id)
   6:      {
   7:          this.id = id;
   8:      }
   9:   
  10:      public Repository Repository { get; set; }
  11:   
  12:   
  13:      public void Run(Context context)
  14:      {
  15:          var customer = Repository.Run(Commands.Fetch<Customer>(id));
  16:   
  17:          if (customer == null)
  18:          {
  19:              context.RunResults(new CustomerNotFoundResults(id));
  20:              return;
  21:          }
  22:   
  23:          var summary = customer.BuildPendingOrdersSummary();
  24:   
  25:          context.RunResults(summary);
  26:      }
  27:  }

 

The class inherits from Task, which is simply an interface that defines void Run(Context). Context is defined here...

   1:  public interface Context
   2:  {
   3:      void RunResults<T>(T results);
   4:  }

 

As you can see, testing the task would be very simple. We would just mock out the repository, and pass a test-specific version of Context to the method, assuring that when the method finishes, context.RunResults was called with the correct argument.

To wrap this all up, an IController class implementation is created when the app starts up. It houses ALL the actions. When it gets a request, it finds the correct action, binds the arguments (in the example, to CustomerID), and calls the lambda function to create the new task. The controller, which has a dependency on IoC container, uses the container to inject the necessary dependencies (like Repository) into the task, and creates a Context with the dictionary of the result types. When the task uses RunResults, the context matches the result type to that passed into the dictionary, and thus we have the ActionResult that we passed in when creating the action.

Another thing I would probably do is change so that the action is built using an action builder, rather than a static Create.Action. Then, you can create an action builder and set a default action (like, for example, if a task runs result UnmetSecurityCriteria, you can always redirect to a page without having to set each action individually, but of course have the ability to override it for a specific action).

I really haven't changed much of my task (typically, I was returning BuildPendingOrdersSummary, but now I'm passing the results into the Context object). I now have a simple way of handling failures without resorting to throwing exceptions. Obviously, not everything is fleshed out. More advanced scenarios could implement their own action result, which might add things to the flash before a redirect, etc., but that still seems like a pain.

Thoughts?

Posted on Sunday, May 11, 2008 1:44 AM MVC , They'll be calling you a radical | Back to top


Comments on this post: Thoughts Regarding Controller Implementation

No comments posted yet.
Your comment:
 (will show your gravatar)


Copyright © mhildreth | Powered by: GeeksWithBlogs.net