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:
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
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.
I thought anonymous types couldn't implement interfaces. So why doesn't the method only accept IRouteConstraint?
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
{
}
I agree that this syntax is not at all discoverable, but once you know it.... (I'd much prefer a fluent interface over this).
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.
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.
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.
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.
Phil,
There is no difference between an API that takes a dictionary of untyped values and anonymous types
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.*"}
Its still in Beta, and overall I think its very well architected.
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.
Ayende,
Another example of ASP.NET MVC making the case that dynamic languages are a better general purpose language.
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.
Just a little bit of Rails love creeping into MVC. The key difference? With Rails I expect to have to use the docs and, luckily, they are great:
api.rubyonrails.org/.../UrlHelper.html#M001190
I'm sure a simple ///<summary> 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.
Andrew,
There are different expectations for different platforms and languages.
This was part of my problem with the HtmlHelper too. The (over)use of anonymous types can lead to usability problems.
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"...
This is just one of those areas we made a trade-off in favor of usability over discoverability.
rickosborne.org/.../usability-vs-discoverability/
www.scottberkun.com/.../26-the-myth-of-discover...
I also found problems in the routing API, I created a custom fluent interface by overloading the Route class:
www.codinginstinct.com/.../...luent-interface.html
www.codinginstinct.com/.../...e-in-mvccontrib.html
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.
I would have done:
routes.Add("Default", "{controller}/{action}/id")
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.
Dang HTML... Should have read "Func(Of String, String)" and "Func(Of String, Bool)", but in C#.
Comment preview