Link here

Link here

App Areas in ASP.NET MVC, take 2

ASP.NET, MVC, Routing 10 Comments »

So the discussion continues: How do you partition an ASP.NET MVC application into separate “areas” or “modules” (e.g., blog module, e-commerce module, forums module), then compose a finished app from those areas or modules?

Phil Haack, ASP.NET MVC program manager, just posted a prototype application structured in that way. It’s a really neat solution, and overcomes a number of issues that others, including myself, have experienced when trying to do it previously.

In this post, I’m going to take Phil’s prototype and tweak it in a few ways to my liking. That’s not to say that there’s anything wrong with his design, but only that I want to throw in some extra ideas that might make it even slicker in some cases.

How it works

The mechanism as given comes in two parts:

  • Routing configuration: There’s an extension method on RouteCollection called MapAreas() which lets you register a URL pattern for multiple areas. You pass it a URL pattern, a controller “root namespace”, and an array of area names. It prefixes “{area}/” to your URL pattern, and for each area name, it registers a route entry that targets controllers who live in the namespace given by: 
    (root namespace + “.Areas.” + area name + “.Controllers”)
  • View engine: There’s a special view engine called AreaViewEngine that uses a built-in convention to look for view templates in an area-specific folder.

Don’t worry if you don’t understand this. If you download Phil’s prototype and try it, you’ll find it’s all straightforward enough.

It works very nicely, and it uses a clever trick with route defaults and constraints so that when you’re generating outbound URLs, you can link to controllers/actions in any area by specifying the area name in your Html.RouteLink() call, or you can link to controllers/actions within the same area using a normal Html.RouteLink() call that doesn’t specify any area name.

Suggestions for enhancement

I’m not going to touch the view engine part of the prototype at all. All I want to achieve is a configuration system that’s slightly more natural (for me) and a modified set of conventions and rules that are a bit more flexible. Here’s how I’d like a simple areas routing configuration to look:

// Routing config for the blogs area
routes.CreateArea("blogs", "AreasDemo.Areas.Blogs.Controllers",
    routes.MapRoute(null, "blogs/{controller}/{action}", new { controller = "Home", action = "Index" })
);
 
// Routing config for the forums area
routes.CreateArea("forums", "AreasDemo.Areas.Forums.Controllers",
    routes.MapRoute(null, "forums/{controller}/{action}", new { controller = "Home", action = "Index" })
);
 
// Routing config for the root area
routes.CreateArea("root", "AreasDemo.Controllers",
    routes.MapRoute(null, "{controller}/{action}", new { controller = "Home", action = "Index" })
);

… and here’s an example of a very slightly more complex configuration:

// Routing config for the blogs area
routes.CreateArea("blogs", "AreasDemo.Areas.Blogs.Controllers",
    routes.MapRoute(null, "SpecialUrlForPosts", new { controller = "Home", action = "Posts" }),
    routes.MapRoute(null, "blg/{controller}/{action}/{id}", new { action = "Index", controller = "Home", id = "" })
);
 
// Routing config for the forums area
routes.CreateArea("forums", "AreasDemo.Areas.Forums.Controllers",
    routes.MapRoute(null, "myforums/SecretAdminZone/{action}", new { controller = "Admin", action = "Index" }),
 
    // Equally possible to construct routes using "new Route()" syntax too
    new Route("myforums/{controller}/{action}", new MvcRouteHandler()) {
        Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index" })
    }
);
 
// Routing config for the root area
routes.CreateArea("root", "AreasDemo.Controllers",
    routes.MapRoute(null, "{controller}/{action}", new { controller = "Home", action = "Index" })
);

The point to notice is that each area is configured independently of the others, which for me feels more natural than defining a single URL pattern that for some reason applies to all areas.

It turns out to be dead easy to make it work like this. All you need is this simple CreateArea() extension method:

public static void CreateArea(this RouteCollection routes, string areaName, string controllersNamespace, params Route[] routeEntries)
{
    foreach (var route in routeEntries)
    {
        if (route.Constraints == null) route.Constraints = new RouteValueDictionary();
        if (route.Defaults == null) route.Defaults = new RouteValueDictionary();
        if (route.DataTokens == null) route.DataTokens = new RouteValueDictionary();
 
        route.Constraints.Add("area", areaName);
        route.Defaults.Add("area", areaName);
        route.DataTokens.Add("namespaces", new string[] { controllersNamespace });
 
        if (!routes.Contains(route)) // To support "new Route()" in addition to "routes.MapRoute()"
            routes.Add(route);
    }
}

Apart from that, there’s no difference from the original prototype (well, you can delete the original prototype’s MapAreas() and MapRootArea() methods: they’re no longer used).

Benefits

How does this differ from the original prototype? What conventions have changed?

  • URL patterns are no longer forced to start with the area name (though they can if you want). Within a single area, you can have some URLs that start with the area name, and some that don’t.
  • In fact, URL patterns are now totally independent of area names, which means your area names can simply be internal code words for software modules, never seen by the public. If that’s what you want.
  • Controller namespaces are independent of area names, and they don’t even have to be constant within a single area. Pick your own convention and follow it.
  • You can  configure each area’s routes in a separate block of code, using a sweet DRY syntax, which for me feels more natural than having a single method call that registers routes across all areas.
  • You don’t need any special code or configuration for the “root” area – that’s just another area like the others, usually just with shorter URL patterns.
  • You can keep using the familiar routes.MapRoute() and new Route() ways of building route entries, merely wrapping up groups of them in CreateArea() calls. No significant new API.

Apart from this, it uses almost exactly the same mechanism as in the original prototype.

Drawbacks

If enforcing conventions is your thing, then you might not appreciate the extra flexibility that comes with these changes.

Also, when you do cross-area links, you have to be careful to specify a controller name and not just assume the default controller will be used. Otherwise, the generated URL might reuse the current request’s controller name, which might not even exist in the destination area. This is because of an obscure technicality in how URL generation works (it doesn’t affect Phil’s original design because of how he requires all URLs to start with an “{area}” segment).

This isn’t worth explaining in detail – all I’ll say is that the solution is simply to make sure your cross-area links always specify a controller name. Of course, you almost certainly should be doing that anyway, because it would be very weird to link just to an action name on a different area without being clear about which controller hosts that action.

Summary

ASP.NET MVC continues to impress me with its flexibility. If you want to structure your app in terms of “modules” or “areas”, it doesn’t take much code to enable it. Phil’s approach to areas is the neatest I’ve seen so far. Personally I like the tweaks I’ve suggested above, but it’s subjective and you can do things your own way.

Adding HTTPS/SSL support to ASP.NET MVC routing

ASP.NET, MVC, Routing 8 Comments »

ASP.NET MVC is introducing a number of brilliant enhancements that make .NET web development much neater than ever before. One of them is the new routing system, which makes it dead easy to handle “clean” URLs, even automatically generating outbound URLs from the same schema.

Unfortunately, as of Preview 4, routing has a missing feature (oh noes!): it’s got no support for absolute URLs. Everything it does works in terms of application-relative “virtual paths“, so you simply can’t generate links to other subdomains, port numbers, or even switch from HTTP to HTTPS (or vice-versa). As they say, the design is never perfect first time.

In many web applications, you do need some way of getting visitors into/out of SSL mode, so routing’s limitation does actually bite you. What I’d like to be doing is marking certain route entries as being “secure”, so they always generate URLs containing https:// (unless the visitor is already in SSL mode, in which case they generate a relative URL, and the non-secure route entries then generate absolute URLs containing http://). That deals with getting visitors in to SSL, and later back out of it automatically.

One way it could work

Perhaps you could configure it like this:

routes.Add(new Route("Account/Login", new MvcRouteHandler())
{
    Defaults = new RouteValueDictionary(new { controller = "Account", action = "Login" }),
    DataTokens = new RouteValueDictionary(new { scheme = "https" })
});
routes.MapRoute("Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" });

With this configuration (notice the DataTokens value), you’d be saying that the Login() action on AccountController must be referenced over HTTPS, whereas all other actions get referenced over HTTP. Which means that:

  • Html.ActionLink(”Click me”, “Login”, “Account”) would render:
    <a href=”https://yoursite:port/virtualDirectory/Account/Login“>Click me</a>
  • Whereas Html.ActionLink(”Click me”, “About”, “Home”) would render:
    <a href=”/virtualDirectory/Home/About“>Click me</a>

… assuming the current request is over HTTP. If the current request was over HTTPS, then it would be the other way round (the first link would be relative, and the second would be absolute) - to get them out of HTTPS.

And what’s so difficult about that?

Firstly, let’s get back to the limitation in System.Web.Routing’s design. It only works in terms of virtual paths. So how can it ever be possible to generate absolute URLs? One option is to create a custom Route subclass, and simply return absolute URLs when you need to (even though you’re only supposed to return virtual paths). It’s a hack, but it might just work. Sadly, life never is that easy.

Problem 1

Some gremlin in the routing underwurlde fiddles with the URL after you’ve generated it, prepending the path to your virtual directory (and also normalizing the URL, e.g. to remove double slashes). What a pain! You’ll generate http://www.google.com, and it gets converted to /http:/www.google.com (notice the leading slash, and the loss of the double slash). That totally wrecks absolute URLs.

Problem 2

How do you make a normal routing configuration aware of a DataTokens entry called scheme, and make it respect it, generating absolute URLs when it needs to? The normal Route class doesn’t have any notion of this. And I don’t want to have to change my routing config in any weird way.

Possible solutions

If RouteCollection was smart enough to spot absolute URLs and not fiddle with them, you’d have solved problem 1. (This is a hint to any MS developers reading :) )

Then, if you used the pseudo-route entry trick I described in my previous post, you could intercept URLs after they’re generated, converting them to absolute URLs when needed. That would solve problem 2.

I’m getting bored. Show me the solution.

Dear lucky reader, here’s an implementation:

1. Start by downloading AbsoluteRouting.zip. It’s a C# class library project. Put it in your solution and compile it yourself. (And obviously, put a project reference from your main MVC project to the AbsoluteRouting project.)

2. In your web.config file, replace UrlRoutingModule with AbsoluteUrlRoutingModule, i.e. put:

<system.web>
    <httpModules>
        <add name="UrlRoutingModule" type="AbsoluteRouting.AbsoluteUrlRoutingModule, AbsoluteRouting"/>
    </httpModules>
</system.web>

3. At the top of your routing configuration, drop an EnableAbsoluteRouting entry, i.e. put:

public class GlobalApplication : System.Web.HttpApplication
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
	// Here it is!
        routes.Add(new EnableAbsoluteRouting());
 
	// Rest of routing config goes here
	routes.MapRoute(...)
    }
}

UPDATE: If you want to use nonstandard ports (i.e. not ports 80 and 443), you can configure it like this:

routes.Add(new EnableAbsoluteRouting()
                 .SetPort("http", 81)
                 .SetPort("https", 450));

4. For any route entries that you want to force into SSL, add a DataTokens entry:

routes.Add(new Route("Account/Login", new MvcRouteHandler())
{
    Defaults = new RouteValueDictionary(new { controller = "Account", action = "Login" }),
    DataTokens = new RouteValueDictionary(new { scheme = "https" })
});

Note that if you don’t specify any scheme value, it will be assumed to demand http (thereby switching visitors back out of SSL mode for links to non-secured routes).

AbsoluteUrlRoutingModule does some nasty weirdness to squish the routing gremlin that destroys absolute URLs, and EnableAbsoluteRouting intercepts the outbound URL generation process to turn relative URLs into absolute ones if it needs to force the link on to a different scheme to the current request. Hopefully, it doesn’t screw anything else up in the process.

Parting comments

I don’t think this is great. It’s a hack; it abuses routing; you’re just not supposed to generate absolute URLs. Also, the code isn’t pretty (read it yourself), and it hasn’t been tested in the real world. It’s just a proof of concept!

However, it does get the job done unintrusively, leaving the rest of your application unchanged (in theory). I’d be interested in anyone else’s ideas about how to deal with the central real-world problem, which is getting visitors into and out of SSL mode in an ASP.NET MVC application. I’d also be interested in hearing any official guidance about how to deal with HTTPS with System.Web.Routing - anyone know of any?

PS: Troy Goode has an idea about how to link to SSL in ASP.NET MVC. His technique is fine (and certainly a lot less hacky than the one I just showed you). However, that technique requires you to fiddle with every Html.ActionLink(), remembering to convert its output when you want an SSL link. You can’t just say in any one central place that a certain action method is supposed to be requested over SSL - not as DRY as I’d like. But as I said, his technique is less hacky.

Site Meter