New MonoRail Routing Engine

October 22nd, 2007

I’ve started a pet project – business project – where SEO can be one of its cornerstones. Albeit I will try to compete with some giants on the brazilian market I’m a firm believer that people tend to look for better products/services and status-quo won’t (always) win.

That and the recently announced MS-MVC (will that be the final name?) brought me back to one of MonoRail’s weaknesses: the routing engine. I confess that albeit we use it in a few projects here, I haven’t really been a great fan of it. To begin with I don’t like regular expressions, and second I don’t like to express things that could be inferred. But haven’t really move a muscle to change anything yet, have I?

So I started this thread on castle-devel which led to great ideas – that’s why I love open source! – and after some more thought I could start developing the routing engine in my spare time. Summing it up I think I’ve spent about 32h to get it to the point it is now: functional but needs some polishing (more real world tests, method overloads, syntax sugar).

As usual, this is bleeding-edge, it’s on MonoRail SVN (not on RC3) and use it at your own risk. If you find problems, please use our issue tracker to submit bugs.

Stating the problem

Suppose you want to offer listing of say, cars. You want that the urls show the data that is being queries on the resource identifier, not through the query string (have you read about REST?).

Something like:

www.cardealer.com/listings/ -> can list all or show a search page, up to you
www.cardealer.com/listings/new/ -> shows a nationwide list of new cars
www.cardealer.com/listings/old/ -> shows a nationwide list of second hand cars
www.cardealer.com/listings/new/ford -> new ford cars
www.cardealer.com/listings/new/toyota -> new toyota cars

What about supporting states and districts?

www.cardealer.com/listings/MD/rockville/ -> shows old and new cars from Rockville, Maryland
www.cardealer.com/listings/MD/rockville/new/ -> only new cars from the same place
www.cardealer.com/listings/MD/rockville/new/toyota -> you got it, right?

So, the urls are self-explanatory. Any user that cares to look at the url/address bar might even start typing things directly as he/she can understand the url and how it relates to narrowing the query.

Now that you understand the problem you should also note that the url semantics above has no ambiguity, and you have to spend some time to create a set of patterns that have no ambiguity at all.

Turning on the MR routing engine

So, first things first, you should getting IIS sending all request to the .Net ISAPI. See the routing document from our document, and please read the suggestion by David Moore. He basically suggests that you have the web site and a separate web site to handle static content (images/css/js). Last time I worked on a big java portal we used JBoss serving the dynamic content and apache httpd serving the static files.

If you don’t want to mess with IIS at this point, you can use the web server that VS.Net offers.

Once this has been done, you should add the routing module definition to your web.config. It’s almost the same thing you done for the old routing module:

540588a90a2561_

The difference is on the type name: Routing.RoutingModuleEx

Cool, now you’re ready to state the url semantics to MonoRail.

Setting up the routing rules

Now it’s just a matter of accessing the routing engine and add rules. Rules are classes that implement IRoutingRule, so you can easily create your own. Out of the box I’m proving two: PatternRule and RegexRule.

PatternRule let you create rules in a less crypt way than using regular expressions. RegexRule let you unleash the devil inside you to create complex rules (just remember to name the matching groups!)

Then add rules on the App_OnStart:

public void Application_OnStart()
{
    RoutingModuleEx.Engine.Add(...);
}

The PatternRule supports a few patterns. It ultimately converts them to Regexp anyway so the performance shouldn’t be bad. The pattern should start with an identifier followed by colon and the pattern name or argument. The following ones are supported:

<id:string>

Means that a string (any string) can be matched.

<id:number>

Means that only a number can be matched.

<brand:apple|cisco|novell>

Means that only one of the following can match. In this case the pattern brand will only match if apple or cisco or novell is present.

*

Greedly matches all.

Now let’s start converting our rules to patterns:

www.cardealer.com/listings/new/

Here you have piece that is fixed and other that can vary (new or old). The PatternRule will be

RoutingModuleEx.Engine.Add(
    PatternRule.Build("bycondition", "listings/<cond:new|old>", typeof(SearchController), "View"));

Basically that should read: add the rule named ‘bycondition’ that should match ‘listing/condition new or old’ and direct that to SearchController, action View.

On your controller, you can receive the values:

public void View(string cond)
{
  RenderText("condition " + cond);
}

The order the rules are tested is important. You should add the most specific rules before the most general ones. For instance

RoutingModuleEx.Engine.Add(
    PatternRule.Build("bycondition", "listings/<cond:new|old>", typeof(SearchController), "View"));
RoutingModuleEx.Engine.Add(
    PatternRule.Build("alllisting", "listings", typeof(SearchController), "Index"));
RoutingModuleEx.Engine.Add(
    PatternRule.Build("unmatch", "listings/*", typeof(SearchController), "RedirectToIndex"));

This tells the engine that it should try to match the ‘bycondition’ rule first, then the simple ‘listings’ path. If nothing matches, it should greedly try to match the last rule which invokes the RedirectToIndex action on your controller.

I’m sure you’re wondering, so here’s why: if an user start ‘exploring the url capabilities’ of your site he might be in a situation that nothing matches. This will lead to ‘alllisting’ nothing matching too. My suggestion is using a greedy rule (*) that is your last resource, and you should redirect the user to something that is know and right – hence you redirect him to a know place.

If a non-matching rule gives HTTP 200 response then you might run into trouble with searching engines too, so be wise.

I digress. Adding more rules will be the same thing, and I left those as exercise.

Bidirectional rules

Once the rules are set, and if you’re a nice guy that always use the helpers for url generation, then you might be happy to know that those are integrated.

For instance, you can generate a form action to a rule:

$Form.FormTag("%{named='bycondition'}")

If the rule takes parameters, then you must pass then using the ‘params’ entry:

$Form.FormTag("%{named='bycondition', params={cond='new'}}")

But why stop there? Suppose at some point in your view you render a list of cars, and you link them to a detail page:

RoutingModuleEx.Engine.Add(PatternRule.Build("cardetail", "listings/show/<id:number>", typeof(CarController), "Show"));

...

#foreach($car in $carslistings)
$Url.Link($car.Headline, "%{named='cardetail', params={id=$car.Id}}")
#end

You can see that the parameter to the rule and the properties on the car type have the same name (Id), so why not take a shortcut:

#foreach($car in $carslistings)
$Url.Link($car.Headline, "%{named='cardetail', params=$car}")
#end

How it works?

At first my plan was that the routing engine just made the matched rule available on Request.Items, so the UrlTokenizer could skip its work and just return what has match. However, due to the damn refactor to introduce cache support on MR, it does check that a request is for really target for it (has the extension that matches the extension bound to the MR handler).

So I took the traditional rewrite path, which is not essentially bad… A request to listing/show/1 will then be converted to /car/show.castle?id=1, nothing magical.

The UrlBuilder service was also slightly changed to use the routing service to generate urls. Please note that the RegexRule is not able to generate urls at the time. Is there anyone out there that want to play with the Regex api and get it done? :-)

More

I also think it can be pretty boring writing rules for all actions on your controller. So I wrote this small utility class that should take care of that:

public class StandardUrlRules
{
	public static void PopulateFor(Type controllerType, RoutingEngine engine)
	{
		ControllerDescriptor desc = ControllerInspectionUtil.Inspect(controllerType);

		PopulateFor(controllerType, desc.Name, engine);
	}
	
	public static void PopulateFor(Type controllerType, string alias, RoutingEngine engine)
	{
		BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
		
		MethodInfo[] methods = controllerType.GetMethods(flags);

		foreach(MethodInfo action in methods)
		{
			string name = action.Name;
			string ruleName = alias + "-" + name;
			string path = alias + "/" + name;

			engine.Add(PatternRule.Build(ruleName, path, controllerType, name));
		}
	}
}

The thing is: the rules that are important to your business should be handcrafted. The others, well, shouldn’t.

You can use it like:

StandardUrlRules.PopulateFor(typeof(SearchController), "listings", RoutingModuleEx.Engine);

Conclusion

Well, up to you :-)

22 Responses to “New MonoRail Routing Engine”

Tuna Toksoz Says:

Keep up the good work, Hamilton
lookingforward to use it after my exams!

dru Says:

Nice hammett! Thanks again. -dru

macournoyer Says:

that is some nice routing! amazing work!

Rik Hemsley Says:

Looks very good. Just wondering why you convert patterns to regular expressions instead of just accepting regexps?

Nick Parker Says:

That looks really nice hammett!

hammett Says:

@Rik Hemsley, IMHO they are not so clear, intention revealing. Just a personal preference but I bet it’s shared by others.

Ken Egozi Says:

Ok.
Very cool to have a newer and cleaner routing module.
However, it seam that a main thing is missing.
in the old module I could’ve a rule that matches several controllers.
It looks as if this thing is binding a rule to a specific controller. Am I right? If so, why did you implement it in that fashion?

hammett Says:

@ken, not following it. can you give examples?

Sean Chambers Says:

excellent job hammett!

keep it up!!

Kevin Williams Says:

It seems to me that

RoutingModuleEx.Engine.Add(
PatternRule.Build(“bycondition”, “listings/”, typeof(SearchController), “View”));

could be written as

RoutingModuleEx.Engine.Add(
“bycondition”, “listings/”, typeof(SearchController), “View”);

either as an alternate method signature or as the only signature. The Add method could call PatternRule.Build internally rather than requiring the user to do it every time. I haven’t looked at the code so I don’t know for sure, but on the surface it seems unnecessary. Other than that, it looks really cool!

hammett Says:

@Kevin, unnecessary how if you can use the PatternRule, the RegexRule or create your own?

Mike Says:

This looks great! One question…
Assuming static content is offloaded to a different site like you suggest, is there any significant difference in performance between using an ISAPI rewriter versus an HttpModule for this?
On my last project I implemented an Isapi rewriter but since monorail isn’t dealing with physical files it seemed to put the flexibility into using a module instead.

Diego Guidi Says:

Impressive!
I’m working right now on a monorail-based RESTful WebService, and i really want to try the new routing features :)

hammett Says:

@Mike, probably yes. The filter are usually written in C, and handcrafted to be fast.

Darius Damalakas Says:

MS MVC did a great job in causing reactions in open source projects.

And that’s why I like free software projects too!
Good to hear that MonoRail has routing

morcs Says:

This looks great, and will be a major influence towards my company adopting MonoRail as our standard for web development.

You mention “please read the suggestion by David Moore”, this sounds like an interesting solution to the problem of having to route all requests to aspnet_isapi, but I can’t find anything about it. Any chance of a link?

hammett Says:
morcs Says:

Thank you :) So obvious in hindsight!

Reshef Mann Says:

Great news!
I looked at the implementation and it looks like that rule matching is done by looping the rules list, can’t it be a performance drawback on a website with lots of pages?

I thought maybe some kind of a trie based rule engine can do the work more efficiently but then the rule won’t be able to encapsulate the matching logic – the engine will need to have access to the rule’s internals.

Thierry Says:

Hi all,

I am trying to implement routing engine on a sample site, but an error occurs during the compilation : ” error CS0234: The type or namespace name ‘Routing’ does not exist in the namespace ‘Castle.MonoRail.Framework’ (are you missing an assembly reference?)”

The namespace Castle.MonoRail.Framework.Routing seems not to work, because I don’t have the assembly.
My problem is about finding this assembly : Where Can I download it ?

Regards
-Thierry-

MS MVC: Routing - The Good, The Bad, The RESTful | Adam Tybor's Blog Says:

[...] 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 [...]

MonoRail Root Directory Routing + RoutingModuleEx Says:

[...] I think this may be one of the few resources detailing how it works: Hammet’s RoutingModuleEx Blog Post. [...]

Leave a Reply