Rails-style controllers for ASP.NET

Rob Conery has been putting together some great screen casts on SubSonic and his latest on generating controllers pointed out that ASP.NET doesn’t support the Rails-style http://site//controller/method style of execution.

This got me quite excited and I’ve put together a proof-of-concept web project that demonstrates mapping the path to controller methods using an IHttpHandler and reflection.

How it works

It registers the ControllerHttpHandler via the web.config:

<httpHandlers>
  <add path="/ctl/\*/\*" verb="POST,GET,HEAD" type="ControllerHttpHandler" />
</httpHandlers>

There is a very basic Controller abstract base class that just provides a simple way of accessing the context for dealing with request/response for now.

public abstract class Controller
{
  protected System.Web.HttpContext context;

  internal Controller(System.Web.HttpContext context) {
    this.context = context;
  }
}

We then have a test controller or two that implement from this with a couple of methods and the required constructor:

public class TestController : Controller
{
  public TestController(System.Web.HttpContext context) : base(context) { }

  public void Index() {
    context.Response.Write("This is the index");
  }

  public void Welcome() {
    context.Response.Write("Welcome to the TestController");
  }
}

Finally the magic that joins them up is the ControllerHttpHandler:

using System;
using System.Web;
using System.Reflection;

public class ControllerHttpHandler : IHttpHandler
{
  public void ProcessRequest(HttpContext context) {
    string[] parts = context.Request.Path.Split('/');
    if (parts.Length < 4) {
      context.Response.Write("No controller & member specified");
      return;
    }

    string controllerName = parts[2];
    string methodName = parts[3];
    Type potentialController = Type.GetType(controllerName);
    if (potentialController != null && potentialController.IsClass && potentialController.IsSubclassOf(typeof(Controller))) {
      MethodInfo potentialMethod = potentialController.GetMethod(methodName);
      if (potentialMethod != null) {
        Controller controller = (Controller) Activator.CreateInstance(potentialController, context);
        potentialMethod.Invoke(controller, null);
      }
      else
        context.Response.Write(String.Format("Method '{0}' not found on controller '{1}'", methodName, controllerName));
    }
    else
      context.Response.Write(String.Format("Controller '{0}' not found", controllerName));
  }

  public bool IsReusable {
    get { return false; }
  }
}

That’s it!

Limitations

The controllers and methods are mapped at run-time using reflection. This would probably be too slow for production. Also it currently has to be in a top-level folder because I can’t figure out how to pass the HTTP request back to ASP.NET to try with the rest of the stack if we don’t have a matching controller/method.

One option might be to have no httpHandlers in the web.config and add the exact controller/method maps at build or run-time. This solves both the top-level problem and potentially the speed.

Another option to address just the speed of reflection would be to cache the path/method strings to the actual method and type so the only reflection would be the Activator.CreateInstance. If that is slow then we could look at pooling the controller instances themselves.

Going forward

Parameters for a method could be extracted and parsed from the query-string – they are currently ignored.

Response is raw output – we could do something very similar to rhtml.

I’m going to chat things over with the Subsonic team and see if we can come up with anything from here.

[)amien

1 responses

  1. Avatar for Rob Bazinet

    Damien,

    This is good stuff. I would like to see how far you go with this.

    In my opinion there has to be an easier way to Rails-like features on .NET than with MonoRail.

    I don't think the SubSonic solution is much in the way of MVC unless the view is the new controller.

    Keep it up.

    -Rob

    Rob Bazinet 3 August 2007