Strongly typed routing for Microsoft MVC4

(Right that title should have put off my casual audience who might have come here expecting a film review or a rant about religion.)

TR;DR: This post describes a new wee .NET library, to add static typing of URL routes in Microsoft MVC4. You can get it on github.

Basically it lets you:

  • Define URL routes as strongly-typed, first-class-objects
  • Bind routes to controller actions, fully statically-checked (so the compiler catches parameter mismatches/misspellings)
  • Generate links in your Razor code (a) succinctly and (b) fully statically type-checked.

Oh, and:

  • It all works at compile time—you don’t need to run a program to generate code or anything like that.

How MVC actions/links currently work

In my previous day job, I worked on a couple of projects with Microsoft MVC4. It’s a very nice, modern web application framework which makes good use of C#. (It’s also Open Source. Good Microsoft! Good Microsoft!) MVC4 makes pretty good use of the type system. In particular, Action methods are type-checked and views are strongly typed (unlike a previous framework I used, MonoRail).

However, one complaint I have of MVC is that there is still a lot of runtime typing, and—well—‘magic’ going on. In particular, links between pages are specified with a syntax like this:

@Html.ActionLink("MyController", "abc", new { x = 1, y = "two" })

This syntax means, ‘create a link to the URL path handled by controller MyController and action abc, passing parameters x = 1, y = “two”’. On the plus side, this decouples the controllers from the URL scheme. On the minus side, there is no check at compile-time that the controller exists or that the action exists, or that it takes these parameters, or that the parameters are of the correct type, or that all required parameters have been provided, or that the action is even mapped to a URL.

On the other side of things, where the URL routing is set up, we might see a routing rule something like this:

routes.MapRoute(
   name: "some-arbitrary-name-for-the-route",
   url: "users/{id}/info",
   defaults: new { controller = "UsersController", action = "GetInfo"},
   constraints: new { id = @"\d+" });

This (horribly verbose syntax) specifies that URL “/users/x/info?p=something” is handled by controller UsersController, action method GetInfo, and that the value for id has to be a string of digits.

[I don’t know why you’re supposed to give names to routes, given that, for debugging you already have the URL and the controller/action to identify it.]

Again, checking is performed at runtime that UsersController.GetInfo exists and that it takes these parameters. (In its defence, the checks are performed early on, at application startup.)

It’s dynamically typed

I don’t like all this dynamic-checking-of-things-which-could-be-statically-checked because:

  1. The compiler is unable to check these things for me, leaving errors to be caught at runtime. This lengthens the write-test-debug cycle.
  2. It’s difficult to mitigate this with testing, since links on pages are not easily unit-tested; form actions even less so.
  3. Refactoring (e.g., renaming methods, adding or removing parameters) is a difficult, manual task, rather than a simple, automated task.
  4. The sophisticated IDE (Microsoft Visual Studio) with its Intellisense is unable to help me autocomplete action names or provide parameters.

In summary, I’m supposed to be programming in a statically-typed language (C#), and MVC4 is turning it into a dynamically-typed language.

Route definitions are inexorably bound up with the Controllers which service them

The framework tries to abstract away from raw URLs; hyperlinks are specified by reference to controllers rather than URLs.

However this means that it’s difficult to move responsibilities between Controllers (as part of a refactoring, for example).

Strongly-typed routes in MVC

So I should probably quit complaining and do something about it. I have. Enter the ‘typed-url-routing’ project, a.k.a. Dysphoria.Net.UrlRouting!

This library allows you to define URL routes as statically-typed, first-class objects. It provides a number of new classes, among them UrlPattern and RequestPattern:

UrlPattern represents a class of URL paths, for example, “/users/x/info?y=something

RequestPattern represents a UrlPattern plus an HTTP method, (for example GET or POST). RequestPattern reifies (represents in concrete form) the notion of an MVC ‘route’.

We can then use UrlPatterns and RequestPatterns to:

  • Register an Action method with a particular route;
  • Generate a link or generate a form element

All uses are strongly typed. For example, here is the definition of the path above:

var userInfoPage = Path("/users/{0}/info?y={1}", Int, AnyString);

The syntax is similar to .NET string formatting syntax. The arguments after the path string specify not only the (C#) type of each argument, but also specify a string pattern which that parameter must match. The first, Int, argument, matches only series of digits and declares the parameter, at compile-time, as an int. The second, AnyString, argument, matches, well, any string, and declares the parameter to be a .NET string.

There are several other parameter specifiers available which specify different patterns. For example, Slug matches an alphanumeric string which may also contain hyphens or underscores; PathComponent matches a wider range of characters, (excluding ‘/’) —and you can easily define your own.

An advantage encapsulating the argument type and its pattern string is that typically a web app will employ a small number of argument types. It is bad engineering to keep repeating the same regex strings for route arguments all over the place.

Defining your own URL parameter types

You can easily define your own URL parameter types simply by subclassing PathComponent<T>, providing a regex string and overriding the methods FromString and ToString.

Using RequestPattern to define routes

Let’s imagine that this URL should accept GET requests (to show the page) and also POST requests (to accept submitted forms). We could specify two RequestPatterns thusly:

var userInfoPage = Path("/users/{0}/info?y={1}", Int, AnyString);

var getUserInfo = Get(userInfoPage);
var submitUserInfo = Post(userInfoPage);

As with conventional MVC, we will have to register actions to act on these routes. Let’s imagine that our controller is called UsersController and that it declares two action methods, GetInfo and SubmitInfo, which handle these two kinds of requests. The ‘wiring’ of the routes to the controller looks like this:

routes.ForController<UsersController>()
   .MapRoute(getUserInfo, uc => uc.GetInfo)
   .MapRoute(submitUserInfo, uc => uc.SubmitInfo);

It’s more-or-less as verbose as the existing MVC mechanism, but has the advantage of being strongly typed.

As with conventional MVC4, the controller with its action methods would look something like:

public class UsersController : Controller {
   public ActionResult GetUserInfo(int id, string p) {...}
   public ActionResult SubmitUserInfo(int id, string p) {...}
}

However, the controller class is completely decoupled from the route definitions, and from any code which wants to refer to these routes. For example:

We might want to make a link to the getUserInfo route’s URL from a Razor view. In conventional (untyped, but strongly-coupled) MVC, it would look like this:

@Html.ActionLink(
   "Link text",
   "UsersController",
   "GetInfo",
   routeValues: new { id = 42, p = "Zaphod" })

Whereas now we can write:

@Html.Link(
   "Link text",
   SiteUrls.GetUserInfo.With(42, "Zaphod"))

…which is not only more succinct, but the compiler will automatically catch any syntax errors or spelling mistakes.

Using it practically

In practice, the argument ‘types’ (PathComponents) are declared statically in an abstract class called Urls. It’s easiest—and promotes good separation of concerns—to define all your URL/Request patterns within a class which inherits from Urls. This class can also contain a method to register all your actions. Comme ça:

using Dysphoria.Net.UrlRouting;

public class SiteUrls : Urls {
   public static readonly UrlPattern<int, string>
      UserInfoPath = Path("/path/to/{0}/page?y={1}", Int, AnyString);

   public static void Register(RouteCollection routes) {
      routes.ForController<UsersController>()
         .MapRoute(Get(UserInfoPath), uc => uc.GetInfo)
         .MapRoute(Post(UserInfoPath), uc => uc.SubmitInfo);
   }
}

Note that in conventional MVC we just declare routes to actions; in this scheme we define URL patterns and then separately map them to actions.

This has the advantage that we can refer to URL patterns separately (e.g., for strongly-typed generation of links). It also means that we can declare URL patterns without associating them with actions. Essentially then the URL patterns declare an interface for a REST Web interface. We could use them to strongly-type an external Web-API which we are writing code against.

Other approaches

Play

The Java/Scala Play Framework has a similar architecture to MVC. However it compiles its route definitions into code in order to allow them to be checked statically. A route definition looks like this:

GET   /users/$id<[0-9]+>/info    controllers.Users.getInfo(id: Int, p: String ?= "")
POST  /users/$id<[0-9]+>/info    controllers.Users.submitInfo(id: Int, p: String ?= "")

This means that a link in a view can be a function call (to the compiled route), so:

@routes.Users.getInfo( 'id → 42, 'p → "Zaphod")

This is a little better than the Microsoft MVC approach, in that the existence of the route is determined statically. The parameters, however, are completely dynamic.

Current status

You can find the code as it exists at the moment here:

https://github.com/bunsen32/typed-url-routing

It compiles and there is a test project which shows how to use it. It’s released under the Apache License 2.0 (same as Microsoft MVC4).

It’s incomplete and pretty untested. I haven’t used it for real in a project yet. So: not only untested, but also unproven!

If you can make use of it, that’s great. However please don’t expect it to ‘just work’. If you would like to fork it, or work it into a larger framework, or even send me patches, that would be great.

If you work for Microsoft and want to merge it into MVC5, be my guest!

Future work

TBH, this library will probably sit dormant until I next have an MVC web app to develop (and I’m mostly using the Scala Play Framework these days).

However, future development would include:

  1. Knocking off the rough corners, and adding a raft of automated tests.
  2. Allowing route composition. We should be able to compose URL paths, allowing a bunch of paths to share a common root.
  3. (Related to route composition), Allowing UrlPatterns and RequestPatterns to refer to external web services. (We could even generate them automatically from existing web service definitions.)
  4. Convention-based Action/Route wiring—where a controller defines action methods matching the signatures of a whole bunch of routes, (according to some naming convention), we could avoid the need to wire up each action to a route, one by one.
  5. Better integration with MVC.

5 thoughts on “Strongly typed routing for Microsoft MVC4

  1. Andy Cohen

    This is exiciting and super slick. Excited to try this out. You should consider proposing this to be added to the mvc source at codeplex.

  2. Fredi Machado

    Thanks Andrew!

    Btw: “[I don’t know why you’re supposed to give names to routes, given that, for debugging you already have the URL and the controller/action to identify it.]”

    It allows you to use @Html.RouteLink(…) with a route name.

  3. Cordell Lawrence

    Looks pretty interesting. I’ve often thought about this problem ever since I started using ASP.NET MVC.

    I must admit that I’ve only scanned through the post. but I will clone the repo, give it a shot and give you some feedback.

  4. Pingback: MVC4 strongly-typed URL-routing… works | Andrew’s Mental Dribbling

  5. Pingback: MVC Recommended Resources And Tutorials | open and free

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.