Editable Routes

UPDATE: I’ve updated this to work in Medium Trust without having to restart the entire App Domain using a technique proposed by my co-worker David Ebbo. I am no longer using the FileSystemWatcher which had problems in Medium Trust

In general, once you deploy your ASP.NET MVC application, you can’t change the routes for your application without recompiling the application and redeploying the assembly where your routes are defined.

routesThis is partly by design as routes are generally considered application code, and should have associated unit tests to verify that the routes are correct. A misconfigured route could seriously tank your application.

Having said that, there are many situations in which the ability to change an application’s routes without having to recompile the application comes in very handy. This is the situation I find myself in as I build a blog engine where the folks who will install may want to tweak the routes without having to recompile the blog’s source code.

In this post, I’ll demonstrate an approach that’ll allow you to define your routes in a content file as code (no XML here!) which you deploy with your application as in the screenshot.

Routes File In Soultion

In my implementation, you need to place the routes in a Config folder in your web root. Note that I used Visual Studio’s Properties dialog to mark the file’s Build Action as “Content” so that it’s not compiled into my application.

Properties

What this means is that the code in the Routes.cs file is not compiled with the application. Instead, we will dynamically compile it. First, let’s look at the contents of that file. It shouldn’t be too surprising.

using System.Web.Mvc;
using System.Web.Routing;
using EditableRoutesWeb;

public class Routes : IRouteRegistrar
{
  public void RegisterRoutes(RouteCollection routes)
  {
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
      "Default",
      "{controller}/{action}/{id}",
      new { controller = "Home", action = "Index", id = "" }
    );
  }
}

One thing you’ll notice is that this class implements an interface named IRouteRegistrar. This is an interface I created and added to my web application (though it could be defined in another assembly).

The code in Global.asax.cs for this application simply calls an extension method I wrote.

protected void Application_Start()
{
  AreaRegistration.RegisterAllAreas();
  RouteTable.Routes.RegisterRoutes("~/Config/Routes.cs");
}

It’s the code in this extension method where the real magic happens.

Before I show the code, there are two concepts at work here that make this work. The first is using the BuildManager to dynamically create an assembly from the Routes.cs file. From that assembly, we can create an instance of the type Routes and cast it to IRouteHandler.

var assembly = BuildManager.GetCompiledAssembly("~/Config/Routes.cs");
var registrar = assembly.CreateInstance("Routes") as IRouteRegistrar;

Once we have an instance of a route registrar, we can call RegisterRoutes on that instance.

The second concept is being able to get notification when the Routes.cs file changes. The clever trick that David told me about is using the ASP.NET Cache object to do that. When you add an item to the cache, you can give it a cache dependency pointing to a file and a method to call when the cache is invalidated.

With those two pieces, we can add a cache dependency pointing to Routes.cs and a callback method which will reload the routes when Routes.cs is changed.

Here’s the full listing for RouteRegistrationExtensions.

public static class RouteRegistrationExtensions
{
  public static void RegisterRoutes(this RouteCollection routes, 
      string virtualPath)
  {
    ConfigFileChangeNotifier.Listen(virtualPath, 
      vp => routes.ReloadRoutes(vp));
  }

  static void ReloadRoutes(this RouteCollection routes, string virtualPath)
  {
    var assembly = BuildManager.GetCompiledAssembly(virtualPath);
    var registrar = assembly.CreateInstance("Routes") as IRouteRegistrar;
    using(routes.GetWriteLock())
    {
      routes.Clear();
      registrar.RegisterRoutes(routes);
    }
  }
}

This uses a class called ConfigFileChangeNotifier which is based on some code David wrote for Dynamic Data.

public class ConfigFileChangeNotifier
{
  private ConfigFileChangeNotifier(Action<string> changeCallback)
    : this(HostingEnvironment.VirtualPathProvider, changeCallback)
  { 
  }

  private ConfigFileChangeNotifier(VirtualPathProvider vpp, 
      Action<string> changeCallback) {
    _vpp = vpp;
    _changeCallback = changeCallback;
  }

  VirtualPathProvider _vpp;
  Action<string> _changeCallback;

  // When the file at the given path changes, 
  // we'll call the supplied action.
  public static void Listen(string virtualPath, Action<string> action) {
    var notifier = new ConfigFileChangeNotifier(action);
    notifier.ListenForChanges(virtualPath);
  }

  void ListenForChanges(string virtualPath) {
    // Get a CacheDependency from the BuildProvider, 
    // so that we know anytime something changes
    var virtualPathDependencies = new List<string>();
    virtualPathDependencies.Add(virtualPath);
    CacheDependency cacheDependency = _vpp.GetCacheDependency(
      virtualPath, virtualPathDependencies, DateTime.UtcNow);
      HttpRuntime.Cache.Insert(virtualPath /*key*/,
        virtualPath /*value*/,
        cacheDependency,
        Cache.NoAbsoluteExpiration,
        Cache.NoSlidingExpiration,
        CacheItemPriority.NotRemovable,
        new CacheItemRemovedCallback(OnConfigFileChanged));
  }

  void OnConfigFileChanged(string key, object value, 
    CacheItemRemovedReason reason) {
    // We only care about dependency changes
    if (reason != CacheItemRemovedReason.DependencyChanged)
      return;

    _changeCallback(key);

    // Need to listen for the next change
    ListenForChanges(key);
  }
}

With this in place, you can now change routes within the Routes.cs file in the Config directory after you’ve deployed the application. Note that technically, a recompilation is happening, but it’s happening dynamically at runtime when the file changes and there’s no need to restart your entire App Domain, which is one benefit of this approach over using the code in App_Code.

If you want to try this code out, you can download a sample project here. The sample app is compiled against ASP.NET MVC 2 RC, but the same principles and code can be used with an ASP.NET MVC 1.0 application. In fact, it can also be used in an ASP.NET 4 Web Forms application since we now support page routing.

Note, if you want to see the old version of this code, I’ve archived it here.

What others have said

Requesting Gravatar... Anthony Bouch Jan 17, 2010 10:53 PM
# re: Editable Routes
Cool, but why would you take this approach over attempting to use an XML file that can be used to create routes declaratively and then making this part of the bootstrap process for the app? More work I guess?
Requesting Gravatar... Vijay Santhanam Jan 17, 2010 10:54 PM
# re: Editable Routes
NICE!! I tried to write something similar for configurable rules that required some code. They were ShopCartPromotions that needed to be added and removed without a redeploy.

I couldn't figure out how to have non-pre-compiled .cs - but you did it!

I ended up with an uber xml config file that was linked to the web.config. when the rules.config changed, the app rebooted and reloaded the config. I wrote waaay too much code to generalize for all possible ShopCartPromotions. Your solution would've been great about 2 months ago...

now can you write a visual studio plugin to jump back in time?
Requesting Gravatar... Aleš Roubíček Jan 18, 2010 12:29 AM
# re: Editable Routes
I have on my project routes in Global Resource file, so they are editable after deployment and are localizable (one language per web instance). I also use Binsor and my own Routes DSL.
Requesting Gravatar... Aleš Roubíček Jan 18, 2010 12:46 AM
# re: Editable Routes
BTW why don't are you using App_Code folder? It has IMO same functionality. :)
Requesting Gravatar... Dhananjay Goyani Jan 18, 2010 4:22 AM
# re: Editable Routes
Nice work. On one hand this gives lot of flexibility to dev over xml based configuration, however on the other side, allowing code injection like this can be harmful in un-controlled environment.
Requesting Gravatar... paul Jan 18, 2010 4:52 AM
# re: Editable Routes
Nice article, Phil.

My team ran into this use case a while back (iirc it was shortly before the 1.0 RTM) and we considered a number of approaches, including something similar to this one (though, like Alec commented above, we were looking at the App_Code directory to solve the file watching for us)

We didn't use XML because we felt it wasn't flexible enough to cover all the routing scenarios

We didn't use the code-generating approach (e.g. thsi one or the App_Code solution) because we felt like it would be too easy to break the app, and too hard to fix it once deployed if the app was so broken.

What we ended up going with was to create a route serializer class, which would actually serialize teh route values along with some metadata, and the logic that updated it would also save teh changes to the db and ensure that the two representations were kept in sync.

Paul
Requesting Gravatar... Dan F Jan 18, 2010 5:00 AM
# re: Editable Routes
P'rhaps a silly question, but is this web site, web app, or both?
Requesting Gravatar... Ira Jan 18, 2010 5:38 AM
# re: Editable Routes
Very cool post Phil. I like the no xml configuration!
Requesting Gravatar... Leniel Macaferi Jan 18, 2010 6:29 AM
# re: Editable Routes
Phil,

I really liked the new point of view of this implementation.

This same path can be used to do a lot of things!

It's so good to be able to do things like that, I mean, the language and the framework has so many features that enable us to do amazing things in a few lines of code. This is developer power... or better yet, power on developer hands...

To monitor a file on disk and recompile the code generating a new assembly dynamically. Fantastic! :)

Thanks for showing us how to do that,

Leniel Macaferi
Requesting Gravatar... haacked Jan 18, 2010 7:24 AM
# re: Editable Routes
@DanF at runtime, there's no difference between WAP and website. But this is a WAP.
Requesting Gravatar... haacked Jan 18, 2010 7:27 AM
# re: Editable Routes
Also, another way to accomplish this scenario is to define routes using a dynamic language. If you'd prefer to use Ruby (ironruby actually) to define routes, I showed how to do that a long time ago. haacked.com/...
Requesting Gravatar... Adam Jan 18, 2010 8:35 AM
# re: Editable Routes
Depending on the size of your site and the amount of traffic you handle, you would want to avoid making your system rely on the App_Code automatic recompile. As confirmed in the MSDN article on dynamic compilation, you would cause a full-site recompile (assuming you haven't tweaked the config settings for it and don't mind those side-effects, ibid). If your site is under any sort of load, that would cause problems for your users while it happens. The approach Phil takes would have a much smaller impact on things.
Requesting Gravatar... haacked Jan 18, 2010 9:28 AM
# re: Editable Routes
@Aleš I could have used the App_Code folder and called BuildManager.GetType("Routes") and that would have worked too.

As Adam points out, that would cause a full AppDomain and Views recycle when you change a route. I wanted to avoid that. It does have the benefit that I don't have to manage my own file monitoring, which is a big benefit. :)
Requesting Gravatar... YP Jan 18, 2010 10:06 AM
# re: Editable Routes
Good blog entry, I believe Phil you have used this approach before in one of your blog.

using the buildManager approach!!!

Thanks,
YP
Requesting Gravatar... Koistya `Navin Jan 18, 2010 10:06 AM
# re: Editable Routes
I would personally avoid this design, there are better ways to archieve the same end result with less trade-offs (like for example storing routes in .xml files or database and reloading routes if necessary without application restart).
Requesting Gravatar... Paco Jan 18, 2010 11:51 AM
# re: Editable Routes
Changing properties of the applications without needing to recompile only has is useful when you don't need a programmer. To configure the routes, you need to know how the controller code looks like, so you probably need a programmer to change the routes like this. This cannot be done by a system administrator. When you need a programmer to make changes, why won't you just deploy a new dll?
Requesting Gravatar... haacked Jan 18, 2010 4:54 PM
# re: Editable Routes
I've updated this post so it works in Medium Trust.
Requesting Gravatar... Aleš Roubíček Jan 18, 2010 9:44 PM
# re: Editable Routes
@Adam in this case you never can deploy new version of your site, cos your App poll will recycle. On hevy load apps, there is a balancer. On hevy load apps there is no unscheduled staging. There is no need to worry about it. App_Code is just fine. :)
Requesting Gravatar... Harry M Jan 19, 2010 5:07 AM
# re: Editable Routes
Hope you're escaping the routes people are giving you at runtime... c# script injection could be a scary thing.
Requesting Gravatar... Travis Illig Jan 19, 2010 7:55 AM
# re: Editable Routes
Correct me if I'm wrong, but I think you may have a tiny memory leak doing it this way.

Assemblies loaded into an AppDomain can't be unloaded without tearing down the whole AppDomain. If you're rebuilding a new tiny temporary assembly each time the code changes, the old tiny temporary assembly with the old code still exists in the AppDomain.

I think I'd go with the App_Code for specifying in code. You eat the restart but there's no memory leak. (I suppose you could craft your solution to build the temporary assembly into a separate AppDomain and marshal the object over, but that sounds like so much work.)

For XML config you could use a CacheDependency with a file dependency and set up a callback to let you know when the config changes. I think CacheDependencies work correctly in medium trust and will correctly monitor the file just like a FileSystemWatcher would. (I haven't tested, though...)
Requesting Gravatar... haacked Jan 19, 2010 11:02 AM
# re: Editable Routes
@Harry, this is still based on code being deployed to your web server. I wouldn't let random people deploy code to my webserver. :)

@Travis that's a good point. I think in general, the number of times someone changes routes in the lifetime of the app will be very small.
Requesting Gravatar... Sam Jan 22, 2010 3:23 AM
# re: Editable Routes
Hi All ,
i am getting the below error , can any one help me out why its not working , i downloaded sample project try to run but it will give me this below error.

Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.
Requesting Gravatar... Imran Baloch Jan 24, 2010 10:29 PM
# re: Editable Routes
Why not use global.asax of one which we used in WebSites(not Web Projects)
Requesting Gravatar... Nathan Taylor Jan 26, 2010 1:32 PM
# re: Editable Routes
I too am receiving 404 errors from the sample (and also after implementing the code myself).

Any advice on how to make this work with area registration as well?
Requesting Gravatar... Jon Ranes Feb 16, 2010 8:14 PM
# re: Editable Routes
I see what you are doing, nice. I was really hoping that I could just add a single route or edit a single route at runtime in MVC 2. We are still tearing all the routes down and rebuilding them here.

Just out of interest what is the big drawback or holdup to editing or adding or removing a single route by reference at runtime?

In an average CMS there would probably be only one or two times a day that new routes needed to be introduced so for now I am sticking with popping the web.config to trigger app_restart.

I'll keep my eye on this method though. I build most of my routes from the database and when I need to change one I pop the web.config.
Requesting Gravatar... Jon Ranes Feb 17, 2010 11:39 AM
# re: Editable Routes
I have found with the code below I can edit or delete a route at runtime perfectly. It is just adding new routes that won't work this way. Sure would be nice to have.


public static void TestChangeRouteRuntime(RouteCollection routes)
{
var routesToRemove = new List<RouteBase>();
foreach (var routeBase in routes)
{
var route = routeBase as Route;
if (route.Url == "cms/{*path}")
{
route.Url = "cms2/{*path}";
routesToRemove.Add(routeBase);
}
}
foreach (var routeToRemove in routesToRemove)
{
routes.Remove(routeToRemove);
}
}
Requesting Gravatar... Jon Ranes Feb 17, 2010 11:56 AM
# re: Editable Routes
Maybe someday we can see the System.Web.Routing source?
# re: Editable Routes
weiter so
Requesting Gravatar... Marcus Mar 11, 2010 6:23 AM
# re: Editable Routes
I am receiving 404 errors when I try to run the sample as well. I think the Routes.cs file is erroring out during the dynamic compile process. Any suggestions?

What do you have to say?

(will show your gravatar)
Please add 4 and 5 and type the answer here: