Ayende @ Rahien

It's a girl

A case study of bad API design: ASP.Net MVC Routing

I am doing a spike in ASP.Net MVC now (and I'll talk about this at length at another time). I hit the wall when I wanted to do something that is trivially simple in MonoRail, limit a routing parameter to be a valid integer.

Luckily, just looking at the API signature told me that this is a supported scenario:

image

Unfortunately, that is all that it told me. This method accept an object. And there is no hint of documentation to explain what I am suppose to do with it. A bit of thinking suggested that I am probably supposed to pass an anonymous type with the key as the route parameter and the value is some sort of a constraint. But what sort of a constraint.

Type information is one of those things that static language actually do, and from experience in both dynamic and static languages, while it is often a PITA to specify types, it actually help for people who read the code. Not often, I'll admit, but it is helpful for the uninitiated.

I am... unused to having this type of problem in C#.

So I did what any developer would do, hit google and tried to find some information about it. Didn't work.

I pulled reflector and started to track down what is going on there. Following a maze of untyped paths that I have not seen the like since the 1.1 days, I finally figured out that the value that I need to push is an instance of IRouteConstraint.

Obvious, isn't it?

In short, and the reason of this post. I am seeing a lot of parameter signatures that look like that, and have barely defined semantics. I would file this under C#.Abuse();

Comments

josh
11/05/2008 07:38 PM by
josh

ouch. I think a person from the MVC team is a reader of yours (not naming names), and is pretty open and responsive. perhaps something can be done.

João Bragança
11/05/2008 07:45 PM by
João Bragança

I thought anonymous types couldn't implement interfaces. So why doesn't the method only accept IRouteConstraint?

Ben Scheirman
11/05/2008 07:46 PM by
Ben Scheirman

Yeah, that isn't at all discoverable.

Since your route values are just arbitrary name/value pairs, it accepts an anonymous dictionary for the constraints, which are typically regex.

something like this:

routes.MapRoute("foo", "blog/archive/{year}/{month}/{day}",

new {controller="blog", action="postsByDate"},

new {year=@"\d[4]", month=@"\d2", day=@"\d2"}

public class BlogController : Controller

{

public ActionResult PostsByDate(int year, int? month, int? day)

{

     ........

}

}

I agree that this syntax is not at all discoverable, but once you know it.... (I'd much prefer a fluent interface over this).

Ayende Rahien
11/05/2008 07:50 PM by
Ayende Rahien

Ben,

There is absolutely no way I would figure out this thing out.

Even from reading the code, I found out that I needed to implement the interface.

Yuck even more.

Jeremy D. Miller
11/05/2008 07:57 PM by
Jeremy D. Miller

I think it's one of those cases where you really need to write your own API wrapper around routing to make it more discoverable. It's a very MS-ish API.

Haacked
11/05/2008 08:43 PM by
Haacked

Then use one of the other overloads take in a RouteValueDictionary. They are very discoverable.

The object overloads was a trade-off we made in that we like the object initializer syntax for its terseness. Sure, it's not discoverable, but the other overloads are discoverable.

I forget who said it, but one rule of usable design I've heard is that if you can't make it discoverable, at least make it memorable. The idea being that now that you know this is the pattern for routes, you'll never forget it.

Haacked
11/05/2008 08:46 PM by
Haacked

Whoops, the MapRoute extension methods don't have overloads that take in a proper RouteValueDictionary. My bad.

I meant, you can use routes.Add(...) which is much more explicit. The MapRoute extensions are a facade for those who like that approach using anonymous objects as dictionaries.

Ayende Rahien
11/05/2008 08:46 PM by
Ayende Rahien

Phil,

There is no difference between an API that takes a dictionary of untyped values and anonymous types

Haacked
11/05/2008 08:48 PM by
Haacked

Also, you can pass in a value as a string, in which case it's a regex, or a value that implement IRouteConstraint for custom constraints. For example:

new {id="\d+", method=new HttpMethodConstraint("GET"), action="foo.*"}

Demis
11/05/2008 09:11 PM by
Demis

Its still in Beta, and overall I think its very well architected.

Andrey Shchekin
11/05/2008 09:24 PM by
Andrey Shchekin

I saw this some time ago, and this is ****the place that makes me careful about 'dynamic' support.

And all what is needed to fix this a method to cast an anonymous type to an interface, like this

routes.MapRoute(..., ..., new { Controller="...", Action="..." }.Cast <i...());

This will allow developer to define some properties as required (part of the interface) and some as optional.

Even simpler would be just to use a fluent interface.

Josh N
11/05/2008 10:04 PM by
Josh N

Ayende,

Another example of ASP.NET MVC making the case that dynamic languages are a better general purpose language.

Stephen
11/05/2008 10:30 PM by
Stephen

Yea its one part of the mvc I've come to hate, its all started on a cute little concept of being able to generate dictionarys from anon objects.. a fluent interface sat on top of standard set of classes would have more code but a lot more understandable.

Neil Mosafi
11/05/2008 11:19 PM by
Neil Mosafi

I'm sure a simple /// comment would have helped here, but I generally hate seeing these kinds of APIs in C# - it s powerful enough that you can constrain people with static types. So much for intention revealing code.

Ayende Rahien
11/05/2008 11:50 PM by
Ayende Rahien

Andrew,

There are different expectations for different platforms and languages.

Jeff Handley
11/06/2008 01:24 AM by
Jeff Handley

This was part of my problem with the HtmlHelper too. The (over)use of anonymous types can lead to usability problems.

configurator
11/06/2008 02:00 AM by
configurator

While I agree that this is a terrible syntax, google is quite helpful in this case. It showed me how to send a regex, how to make a custom constraint, and even how to use HttpMethodConstraint to put a constraint on the HttpMethod (post or get). And all this just because I read this blog post and googled "mvc routing constraints"...

Steve Wagner
11/06/2008 03:02 PM by
Steve Wagner

I think this is only a problem of documentation. If these method are get an nice in line help which show you how to use it, its all not so hard as well.

Paul Stovell
11/07/2008 03:05 AM by
Paul Stovell

I would have done:

routes.Add("Default", "{controller}/{action}/id")

.WithDefaults(controller => "Home", action => "View")

.WithConstraints(

    controller => controller != "Secret", 

    action => Regex.Match(action, "del*"), 

    id => id >= 0);

WithDefaults would be declared as taking a params array of Func <string,> . WithConstraints would take a params array of Func <string,> . The funcs would be auto-wrapped into an IConstraint or whatever.

Intellisense would say: this takes a list of lambda's that accept a string and return a bool. The only implied part is that the "key" of the lambda must match the parameter name.

Paul Stovell
11/07/2008 03:07 AM by
Paul Stovell

Dang HTML... Should have read "Func(Of String, String)" and "Func(Of String, Bool)", but in C#.

Comments have been closed on this topic.