ASP.NET MVC: class / action, not method / action

28.09.2009 - 08:13 (4 years, 10 months, 1 day ago)

programming, TrivadisContent, software architecture, C#, StructureMap, ASP.NET MVC

While pushing along my little home-brew CMS, Rf.Sites, which uses ASP.NET MVC for the HTTP Cruft that comes with Web development, there was something that was disturbing me. The train of thought was roughly as follows:

  1. Hm, when I show content by id (URL e.g. Content/Entry/1), I may have other dependencies than some other Content request.
  2. Since both methods may live in the same controller I may inject dependencies that may not be used for the current request. That's kinda silly. Construction is not that expensive, but it's sort of awkward because I only call one method / controller instantiation...
    Is it service location for lazy loading of dependencies then? Fair enough, but testing the controller means to provide an IContainer.
  3. In my current work project that uses WinForms we have a pretty clear map of class (Command) / user interaction. I do like that...can't we have that in ASP.NET MVC as well?
  4. Think...more think. Isn't the ASP.NET MVC source code available? What does MVC actually do to call the Controller method when I use a certain URL?

First off: Having source code available for MVC is terrific. Best documentation there is. It seems to me that the developers have made an effort to provide decent code. I.e. understandable, a little bit of duct tape, and abstractions where you might expect them. You can agree or disagree with decisions taken for that framework, but code availability and a clean layout are really major selling points in my eyes.

In the MVC source code I found the notion of the Action Invoker. This is the code responsible for calling the appropriate action for an incoming request. The Controller class already has a way in place to exchange the Action Invoker. To trigger it, I need to do this:
public class ActionDispatcher : Controller
{
  public ActionDispatcher(IActionInvoker invoker)
  {
    ActionInvoker = invoker;
  }
}

How this controller can be instantiated has already been explained here

In my DI container of choice, I then register what IActionInvoker is:

ForRequestedType<IActionInvoker>()
  .TheDefaultIsConcreteType<UrlToClassActionInvoker>();
How does said action invoker look like? Here's the main method:
public bool InvokeAction(
  ControllerContext controllerContext, 
  string actionName)
{
  string instanceKey = formTheName(controllerContext);

  var action = container.With(controllerContext)
    .GetInstance<IAction>(instanceKey);
  var result = action.Execute();
  result.ExecuteResult(controllerContext);
  return true;
}

IAction is my own abstraction and looks like that:

public interface IAction
{
  ActionResult Execute();
}

The instanceKey essentially concatenates the request such that Content/Entry becomes ContentEntry.

What is needed now is to register all IAction implementations based on said convention...

Scan(
  s =>
  {
    s.AssemblyContainingType<SiteRegistry>();
    s.AddAllTypesOf<IAction>()
      .NameBy(t => t.Name.Replace("Action", ""));
  });

Now, if I want to handle a call to Entry under Content, I specify an Action called ContentEntryAction.

Arguments to those actions are encapsulated as concrete classes. Since the ControllerContext is provided to StructureMap when obtaining an IAction instance, any class can express a dependency to this concrete instance and obtain the one added for this specific instance resolution. Since the Arguments class is also a concrete class, there is no need to explicitly register those in StructureMap. I have e.g. these Arguments...
public class RequestByIDActionArgs
{
  public RequestByIDActionArgs() { }

  public RequestByIDActionArgs(ControllerContext ctx)
  {
    var routeDataValues = ctx.RouteData.Values;
    Id = int.Parse(routeDataValues["val1"].ToString());
  }

  public int Id { get; set; }
}
Which are then fed to the action through the constructor:
public class ContentEntryAction : AbstractAction
{
  private readonly RequestByIDActionArgs args;
  private readonly IRepository<Content> repository;

  public ContentEntryAction(
    RequestByIDActionArgs args, 
    IRepository<Content> repository)
  {
    this.args = args;
    this.repository = repository;
  }

  public override ActionResult Execute()
  {
    var content = repository[args.Id];
    return createResult(new ContentViewModel(content));
  }
}

I am aware that for now I've cut myself off from all features that come into MVC through the ActionInvoker abstraction. As far as I can understand the code this is the handling of all those cutesy filters, authorizations and similar attributes with which you can decorate your controller methods. I'll see how to reintroduce them once I need it.

For now I am happy that I was able to introduce a class / action pattern without much effort - I don't have a "controllers" folder in my MVC project anymore, instead I have an "actions" folder.