(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:
- The compiler is unable to check these things for me, leaving errors to be caught at runtime. This lengthens the write-test-debug cycle.
- It’s difficult to mitigate this with testing, since links on pages are not easily unit-tested; form actions even less so.
- Refactoring (e.g., renaming methods, adding or removing parameters) is a difficult, manual task, rather than a simple, automated task.
- 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 UrlPattern
s and RequestPattern
s 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 RequestPattern
s 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’ (PathComponent
s) 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:
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:
- Knocking off the rough corners, and adding a raft of automated tests.
- Allowing route composition. We should be able to compose URL paths, allowing a bunch of paths to share a common root.
- (Related to route composition), Allowing
UrlPatterns
andRequestPatterns
to refer to external web services. (We could even generate them automatically from existing web service definitions.) - 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.
- Better integration with MVC.
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.
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.
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.
Pingback: MVC4 strongly-typed URL-routing… works | Andrew’s Mental Dribbling
Pingback: MVC Recommended Resources And Tutorials | open and free