Code Voyeur
RSS
Data Access Languages MVC ORM About Roadmap Contact Site Map RSS Sample Code Presentations Snippets dll Hell .net Rate My Snippet

A Simple IronPython Route Mapper for ASP.NET MVC

Route mappings are part of any ASP.NET MVC configuration. The default Visual Studio MVC template adds a RegisterRoutes method to the Global.asax where all (non-Area) routes are expected to live. Nicely formatted routes might take five lines each. An MVC site with only a few routes quickly starts to clutter the Global.asax. It is easy enough to move the standard mappings into another class or bury them below the fold. But the problems of compiled configuration still exist.

routes.MapRoute(
    "Default",                                              // Route name
    "{controller}/{action}/{id}",                           // URL with parameters
    new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
);
Route mappings are reasonably static entities and there is certainly an argument for keeping them in an app's compiled codebase. However, routes are theoretically reusable and could even require modifications for different deployment destinations. Admittedly, these are probably uncommon cases. This article will show how to create a simple route manager that will read rules configured in an IronPython script.

The RouteManager class will be responsible for reading the script and interpreting the rules. It has only one static method, RegisterRoutes.


public class RouteManager {

    ...    

    public static void RegisterRoutes(string path = null) {
      ...
    }
   
}
Like other IronPython experiments on CodeVoyeur, this class contains static classes for caching the required scripting API class instances. These instances are initialized in a static constructor.

private static Dictionary<string, object> _routes = new Dictionary<string, object>();
private static ScriptRuntimeSetup _setup = null;
private static ScriptRuntime _runtime = null;

static RouteManager() {
    _setup = ScriptRuntimeSetup.ReadConfiguration();
    _runtime = new ScriptRuntime(_setup);
    _runtime.LoadAssembly(Assembly.GetExecutingAssembly());
}
RegisterRoutes starts off by checking for a supplied config file path, otherwise it assumes a routes.py file is in the App_Data directory.
path = path ?? HttpContext.Current.Server.MapPath("~/App_Data/routes.py");
_runtime.GetEngine("IronPython").ExecuteFile(path, _runtime.Globals);
The format of the Python config file is to have a function defined per rule.
def posts():
  pass

def default():
  pass
The only global variables allowed in the file are these functions/mappings. Using that requirement it is possible to gather all of the function names from the file.
var variables = _runtime.Globals.GetVariableNames().Where(s => ! s.StartsWith("__") && ! s.EndsWith("__"));
Iterating over the set of mapping function names, each function is invoked.
foreach (var variable in variables) {                

    var route = _runtime.Globals.GetVariable(variable);
    var routeInfo = _runtime.Operations.Invoke(route) as PythonTuple;

    ...
 
}
The mapping functions return a tuple, where the return values are the mapped url, route defaults and route constraints (in that order).
def default():
  url = "{controller}/{action}/{id}"
  defaults = { "controller" : "Home", "action" : "Index", "id" : "" }
  constraints = {}

  return url, defaults, constraints
The tuple is validated to verify that it contains the expected values. If it does not, an exception is thrown.
validateRouteInfo(routeInfo);
private static void validateRouteInfo(PythonTuple routeInfo) {
          
  if (routeInfo.Count != EXPECTED_TUPLE_LENGTH) {
      throw new RouteMappingException();
  }

  if (routeInfo.Contains(null)) {
      throw new RouteMappingException();
  }
}
The defaults and constraints variables each contain a PythonDictionary instance. These dictionaries are mapped to RouteValueDictionary instances.
Func<PythonDictionary, RouteValueDictionary> routeDictFromPyDict = delegate(PythonDictionary pythonDict) { 
    var routeValueDict = new RouteValueDictionary();
    foreach (string key in pythonDict.Keys) {
        routeValueDict[key] = pythonDict[key];
    }
    return routeValueDict;
};
Finally, the routes are added to the RouteTable.Routes collection.
RouteTable.Routes.Add(variable, new Route(url, routeDictFromPyDict(defaults), routeDictFromPyDict(constraints), new MvcRouteHandler()));
Global.asax contains only the call to the static RegisterRoutes method.
protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();
    RouteManager.RegisterRoutes();            
}
The sample app contains a more elaborate mapping, complete with constraints.
def posts():
  from System import *
  url = "Posts/Index/{year}/{month}/{day}/{title}"
  defaults = { 
    "controller" : "Posts", 
    "action" : "Index", 
    "year" : DateTime.Now.Year.ToString(), 
    "month" : DateTime.Now.Month.ToString(), 
    "day" : DateTime.Now.Day.ToString(), 
    "title" : "" 
    }
  constraints = { "year" : "\d{4}", "month" : "\d{1,2}", "day" : "\d{1,2}" }

  return url, defaults, constraints
With this mapping, it is possible to create a blog posting-like URL such as /Posts/2010/7/6/A-Simple-Route-Mapper-for-ASP-NET-MVC. The contrived example below demonstrates that the route data is present as expected..
<h2><%: Server.UrlDecode(RouteData.Values["Title"] as string) %></h2>

<div>
    Posted on <%: new DateTime(int.Parse(RouteData.Values["Year"] as string), 
                               int.Parse(RouteData.Values["Month"] as string), 
                               int.Parse(RouteData.Values["Day"] as string)).ToString("MMMM dd, yyyy") %>
</div>

Download Sample Project

Article Posted: Tuesday, July 06, 2010

Leave a Comment

Shout it

Contact Code Voyeur about this article.