MS MVC: Routing – The Good, The Bad, The RESTful

After playing with MS MVC for a little bit, and looking at routing extensively I have compiled a list of good things and things that need some improvement. The team has already said they will be doing work with routing, I am looking forward to seeing what they come up with.

What’s Good with MS MVC Routing?

Not Bound Explicitly To Controllers and Actions

An innovative approach to routing was not forcing the controller and action to be bound to the route. Instead they introduced two special parameters [controller] and [action] to indirectly bind the request. This drastically reduces the number of routes your application needs and means you do not have to register a route for every new controller.

Validation

Validation separates the url from matching on the parameters. It allows for full blown regular expressions without dirtying up the url and making it more difficult to generate urls from the routes. Hammett ran into this issue with his new routing module for monorail when he offered a straight regex rule. There was no way to pull parameters out of it.

Route Handlers

A very simple way to extend route resolution on incoming requests. Its not complicated and it gives you access to everything you need to do custom matching. I wish we could get something like that for generating urls.

Restful

Not much more to say hear that hasn’t already been said. This engine does make writing restful urls a breeze.

Testing

Thank you for making my routes so easy to test. I now have confidence that everything is working without having to fire up IIS and a browser to test by hand.

What’s Not So Good with MS MVC Routing?

No Duplicate Controller Names?

I can’t have two controllers in different namespaces with the same name?

Solution

Phil said they are fixing this. I would hope I could specify a Controller parameter either as a Type or a String where the string could be the full name of the controller. I would also maybe like this to be addressed with Areas… maybe.

Areas Where Are You?

There is no out of the box support for Areas. An area is simply a partition for your application. The most common one I can think of is http://localhost/admin/[controller] for separating all of your admin stuff from regular url’s.

Another scenario would be dropping in secondary applications, like a forum, blog, photo gallery, etc. Those will probably need to be defined under some Area.

Workarounds

RouteTable.Routes.Add(new Route
{
 Url = "admin/[controller]/[action]",
 Defaults = new
 {
  Controller = "Admin",
  Action = "Index"
 },
 Validation = new
 {
  Controller = "Admin|Users|Categories"
 },
 RouteHandler = typeof(MvcRouteHandler)
});

What we did above is hard code are “admin” area and then set a Validation regular expression on Controller. Now only the controllers listed will be accessible via those urls, and when generating a url it will have the correct prefix of “admin/” in the path.

What sucks about that? Every time I add a new controller that should be an admin controller I have to modify the regular expression. BTW, that regex is not case insensitive like the url.

Potential Solutions

  1. Allow a convention where the namespace dictates the area.
  2. Add an AreaAttribute similar to Monorail.

I like having the option of number 1, but I don’t want to be forced to this. It makes my urls feel to tightly coupled to my code. Its not a bad default convention as I am sure it will work the vast majority of the time.

Number 2 just doesn’t sit right with me but it seems like a decent option as well. The area has to come from somewhere, right?

One thing is for sure, there needs to be some type of Area parameter that is respected by the RouteCollection and based on the current implementation it looks like it would be very difficult for it to support areas deeper than 1 level. By default it is splitting on the “/” so an area covering “admin/maintenance” would be difficult to use with a single Area parameter.

Maybe it could support arrays which could get interesting…

[area]/[area]/[area]/[controller]/[action]
Defaults = new { Area = "admin/maintenance/daily" }
Validation = new { Area = "admin/maintenance/daily" }

Are We Really Blocking and Locking?

Reflecting over the code of the RouteCollection we can see that when an operation is performed like GetRouteData or GetUrl then the routes are locked until the proper route is matched. Every request coming into your application is going to call GetRouteData and every link you render in your views is going to call GetUrl. What does this mean. It means when a request comes in it locks the routes searching for a match. When a new request comes in on a separate thread it calls GetRouteData too and sits and waits until the first request found a match before it can proceed. And all the blocking happens for every link that you are rendering in your views too. Most pages will have 10 – 20 links, that’s lot of blocking, idle time, and context switches for the OS.

Solution

Don’t lock, period. Adding routes should go into a temp route collection. Add explicit methods for Build() or Rebuild() which can perform an atomic assignment operation on the temp routes to the routes in use. Can some kind of error happen if routes are switch in the middle of processing a request? Yes. What are the odds? Slim. If dynamically altering routes is something that my application requires, without restarting it, then let me handle that. Its an edge case.

If you have to lock, think about using a lock free collection, or use Reader/Writer locks as the number of reads is going to demolish the number of writes. Locking is just way too expensive to do on every request and every link render.

Why is Validation Case Sensitive?

Urls should be case insensitive. If I want to validate parameters using a regular expression those should be case insensitive to. Please don’t make me write Validation expression like: @”[eE][dD][iI][tT]” to match any combination of the string “Edit”.

Why is Validation Not Compiled?

Every request coming in is going to call GetRouteData, does it not make sense to compile my Validation expressions so they do not need to parsed every time they are evaluated?

Solution

if (!Regex.IsMatch(input, pattern))
{
 return false;
}

to

foreach(Regex validator in route.Validators)
{
 //...
 if(!validator.IsMatch(input))
 {
  return false;
 }
 //...
}

Where is the Extensibility?

No, not this extensibility, well maybe, give me the option please.

There are a number of edge cases popping up on blogs, comments, and forum posts.

  • What do I do if I need a different sub/domain?
  • What do I do if I need to redirect to a secure url from an insecure one?
  • What do I do if I need an absolute url instead of a relative one?
  • What do I do if I need to redirect to a non-standard port for https?

While the MVCRouteHandler is extendable the Route is not, nor is the RouteCollection. We need a way to override the matching AND url generating for specific rules.

Potential Solution

The RouteCollection looks a little heavy. I think it can delegate some of its responsibility to other services whose implementations can be swapped out. I also think it would be prudent to give us some interfaces and factories. Just overriding the GetRouteData and GetUrl is not enough. It would be great to have the ability to create our own RouteCollection so we could implement different algorithms for collecting and enumerating the routes to find a match?

Summary

All in all I am very pleased with routing in the first CTP. IMHO it is better than Monorail routing right now. It does need some pieces re-thought, re-factored, and de-debugged, but if it were perfect it wouldn’t be CTP and I wouldn’t be writing this.

Please let me know what I am missing and how crazy some of my solutions are.

+1 To the MS-MVC team, they are off to a great start.

MS MVC: Simply Restful Routing

MS MVC first public code drop is available. I hope you have read through all of ScottGu’s posts to help prep you for the drop. If you haven’t I highly suggest reading about Routing first before you dive into this.

What we are going to do is build a Simply Restful Route Handler similar to what is available in rails. MS MVC supports restful routing pretty good out of the box. The only case it doesn’t pick up is when browsers cannot send real HTTP PUT and DELETE requests. To get around this we are going to build a very simple route handler to inspect a hidden field on the submitted form to set the intended action of the request.

We will be creating 8 restful routes, contrary to the 7 routes in simply restful.

The Eight Actions

  • Show : handles a GET request for a displaying a single resource that the controller is representing.
  • Create : handles a POST request for a creating a new resource.
  • Update : handles a PUT request for updating an existing resource.
  • Destroy : handles a DELETE request on a resource.
  • Index : handles a GET request for a collection of resources.
  • New : handles a GET request for a blank form for creating a new resource
  • Edit : handles a GET request for a form with values filled in from the resource for updating.
  • Delete : handles a GET request a confirmation / form with options for deleting a resource. * This is the extra action.

The Routes

Action Url Http Method Form Method
Show [controller]/[id] GET
Create [controller] POST
Update [controller]/[id] PUT
Update [controller]/[id] POST PUT
Destroy [controller]/[id] DELETE
Destroy [controller]/[id] POST DELETE
Index [controller] GET
New [controller]/new GET
Edit [controller]/[id]/edit GET
Delete [controller]/[id]/delete GET

The Simply Restful Routes In MS MVC

routeCollection.Add(new Route
{
  Url = "[controller]/new",
  Defaults = new { Action = "new" },
  RouteHandler = typeof(MvcRouteHandler)
});
routeCollection.Add(new Route
{
  Url = "[controller]/[id]/[action]",
  Validation = new
  {
    Method = "GET",
    Id = idValidationRegex ?? MatchAny,
    Action = "[eE][dD][iI][tT]|[dD][eE][lL][eE][tT][eE]"
  },
  RouteHandler = typeof(MvcRouteHandler)
});

routeCollection.Add(new Route
{
  Url = "[controller]/[id]",
  Validation = new
  {
    Method = "POST",
    Id = idValidationRegex ?? MatchAny,
  },
  RouteHandler = typeof(SimplyRestfulRouteHandler)
});

routeCollection.Add(new Route
{
  Url = "[controller]/[id]",
  Defaults = new { Action = "show" },
  Validation = new
  {
    Method = "GET",
    Id = idValidationRegex ?? MatchAny,
  },
  RouteHandler = typeof(MvcRouteHandler)
});

routeCollection.Add(new Route
{
  Url = "[controller]/[id]",
  Defaults = new { Action = "update" },
  Validation = new
  {
    Method = "PUT",
    Id = idValidationRegex ?? MatchAny
  },
  RouteHandler = typeof(MvcRouteHandler)
});

routeCollection.Add(new Route
{
  Url = "[controller]/[id]",
  Defaults = new { Action = "destroy" },
  Validation = new
  {
    Method = "DELETE",
    Id = idValidationRegex ?? MatchAny
  },
  RouteHandler = typeof(MvcRouteHandler)
});

routeCollection.Add(new Route
{
  Url = "[controller]",
  Defaults = new { Action = "index" },
  Validation = new { Method = "GET" },
  RouteHandler = typeof(MvcRouteHandler)
});

routeCollection.Add(new Route
{
  Url = "[controller]",
  Defaults = new { Action = "create" },
  Validation = new { Method = "POST" },
  RouteHandler = typeof(MvcRouteHandler)
});

I am not going to explain much of what is going on above, have a look at ScottGu’s post on routing for that. What I will point out are a couple things. First we use use some hard matches like [controller]/new and map that with a Default action. Very straightforward but just a good example of how Routes and Defaults work. Second we add some validation. Scott covered the standard validation that accepts a regex. You will notice that the regex is case sensitive so we end up with a pretty wacky looking string for “Edit|Delete”. Third, we are using a special validation property named Method which will match on the HttpMethod of the Request. And finally notice we only have a single rule that will require a custom IRouteHandler.

So lets start with a story for our new custom route handler:

Story: RouteHandler Assigns Restful Actions

As a Route Handler
I want standard post requests to route to restful actions.
So that people without javascript enabled or incompatible browsers can still have a restful experience and my web server needs to issue less browser redirects.

Acceptance Criteria:

Scenario 1: Form _method PUT Triggers Update Action
GIVEN the form has a field named _method with a value of PUT
AND the url matches [controller]/[id]
AND the HTTP method is POST
WHEN the form is posted
THEN the route action should be Update

Scenario 2: DELETE Triggers Destroy Action
GIVEN the form has a field named _method with a value of DELETE
AND the url matches [controller]/[id]
AND the HTTP method is POST
WHEN the form is posted
THEN the route action should be Delete

TDD First

To make testing easier I will be using Rhino.Mocks to create dynamic mocks of the IHttpContext and IHttpRequest. Thanks Microsoft for finally giving us interfaces to make our testing easier.

[TestFixture]
[Category("SimplyRestfulSpecs")]
public class When_The_Form_Is_Posted_With_A_Form_Field_Named_Method_And_A_Value_Of_PUT
: BaseRouteHandlerTestFixture
{
[SetUp]
protected override void GivenSetupContext()
{
  base.GivenSetupContext();
  form.Add("_method", "PUT");
}

[Test]
public void Then_The_Route_Action_Should_Be_Set_To_Update()
{
  RestfulAction action = RestfulAction.None;
  IRestfulActionResolver resolver = new RestfulActionResolver();

  using(mocks.Record())
  {
    SetupResult.For(httpContext.Request).Return(httpRequest);
    SetupResult.For(httpRequest.RequestType).Return("POST");
    SetupResult.For(httpRequest.Form).Return(form);
    requestContext = new RequestContext(httpContext, routeData);
  }

  using(mocks.Playback())
  {
    action = resolver.ResolveAction(requestContext);
    Assert.That(action, Is.EqualTo(RestfulAction.Update));
  }
}
}

[TestFixture]
[Category("SimplyRestfulSpecs")]
public class When_The_Form_Is_Posted_With_A_Form_Field_Named_Method_And_A_Value_Of_DELETE
: BaseRouteHandlerTestFixture
{
[SetUp]
protected override void GivenSetupContext()
{
  base.GivenSetupContext();
  form.Add("_method", "DELETE");
}

[Test]
public void Then_The_Route_Action_Should_Be_Set_To_Destroy()
{
  RestfulAction action = RestfulAction.None;
  IRestfulActionResolver resolver = new RestfulActionResolver();

  using (mocks.Record())
  {
    SetupResult.For(httpContext.Request).Return(httpRequest);
    SetupResult.For(httpRequest.RequestType).Return("POST");
    SetupResult.For(httpRequest.Form).Return(form);
    requestContext = new RequestContext(httpContext, routeData);
  }

  using (mocks.Playback())
  {
    action = resolver.ResolveAction(requestContext);
    Assert.That(action, Is.EqualTo(RestfulAction.Destroy));
  }
}
}
  #region BaseRouteHandlerTestFixture
  public abstract class BaseRouteHandlerTestFixture
  {
    protected MockRepository mocks;
    protected IHttpContext httpContext;
    protected IHttpRequest httpRequest;
    protected RouteData routeData;
    protected RequestContext requestContext;
    protected NameValueCollection form;

    protected virtual void GivenSetupContext()
    {
      mocks = new MockRepository();
      httpContext = mocks.DynamicMock<IHttpContext>();
      httpRequest = mocks.DynamicMock<IHttpRequest>();

      routeData = new RouteData();
      routeData.Values.Add("controller", "testcontroller");
      routeData.Values.Add("action", "update");

      form = new NameValueCollection();
    }
  }
  #endregion

The Code

public enum RestfulAction
{
None = 16384,
Show = 1,
Create = 2,
Update = 4,
Destroy = 8,
Index = 16,
New = 32,
Edit = 64,
Delete = 128
}

public class SimplyRestfulRouteHandler : MvcRouteHandler
{
private const string MatchAny = “*“;

private IRestfulActionResolver actionResolver;

public SimplyRestfulRouteHandler()
{
actionResolver = new RestfulActionResolver();
}

public SimplyRestfulRouteHandler(IRestfulActionResolver actionResolver)
{
this.actionResolver = actionResolver;
}

protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
EnsureActionResolver(requestContext.HttpContext);

RestfulAction action = actionResolver.ResolveAction(requestContext);
if (action != RestfulAction.None)
{
requestContext.RouteData.Values["action"] = action.ToString();
}
return base.GetHttpHandler(requestContext);
}

private void EnsureActionResolver(IServiceProvider serviceProvider)
{
if (actionResolver == null)
{
actionResolver = (IRestfulActionResolver)
serviceProvider.GetService(typeof(IRestfulActionResolver));
if (actionResolver == null)
actionResolver = new RestfulActionResolver();
}
}
}

public interface IRestfulActionResolver
{
RestfulAction ResolveAction(RequestContext context);
}

public class RestfulActionResolver : IRestfulActionResolver
{
public RestfulAction ResolveAction(RequestContext context)
{
if (context == null ||
context.HttpContext == null ||
context.HttpContext.Request == null)
{
throw new NullReferenceException(“Request in current HttpContext cannot be null.“);
}

if (string.IsNullOrEmpty(context.HttpContext.Request.RequestType))
{
return RestfulAction.None;
}

string requestType = context.HttpContext.Request.RequestType.ToLowerInvariant();
if (string.Equals(requestType, “post“, StringComparison.Ordinal))
{
return ResolvePostAction(context);
}

return RestfulAction.None;
}

private static RestfulAction ResolvePostAction(RequestContext context)
{
if (context.HttpContext.Request.Form == null)
{
return RestfulAction.None;
}

string formMethod = context.HttpContext.Request.Form["_method"];
if (string.IsNullOrEmpty(formMethod))
{
return RestfulAction.None;
}

formMethod = formMethod.Trim();
if (string.Equals(“put“, formMethod, StringComparison.OrdinalIgnoreCase))
{
return RestfulAction.Update;
}
else if(string.Equals(“delete“, formMethod, StringComparison.OrdinalIgnoreCase))
{
return RestfulAction.Destroy;
}

return RestfulAction.None;
}
}

Conclusion

The only opinion I have so far on MS-MVC is it beats the heck out of WebForms and I am glad to see MS is committed to supporting a product that fits this space. In the coming weeks I should have much more about the framework. I just wanted to get something usable out to the community first.

I hope to get this stuff into the new MVC Contrib project as well.

Whats next, maybe some form helpers, templates, and a base controller to make working with the views and actions a little easier.