Database driven MVC Routing

I’m not overly keen on hard coding MVC routes in the application, so I’ve come up with a nice way of pulling the routes out of the application, and into a Sql Server table. All nicely wrapped up with Linq-to-Sql.

We begin, by planning our database tables. We need to be flexible enough to store both the routes, and route parameters, so that’s exactly what we do. Our Route table can hold both active and ignored routes, here’s my design:

Routes have parameters, but parameters also have defaults and constraints (we don’t really cover constraints with this version, but we can advance on that in the future). One of the difficulties establishing routes outside of code, is denoting the type of the parameter. I’ve designed the RouteParameter table to take this into consideration:

Then with a bit of added drop and drag, we can create out Linq-to-Sql entities:

How does this all tie up? Well, when the application starts, we can then poll our database for routes to add. We create a registration method, which accesses our DataContext and pulls out any routes. We handle our ignore routes first, and then create our active routes.

/// /// Registers the configured routes. ///
public void RegisterRoutes()
{
using (DataContext context = DataContext.CreateDataContext())
{
var allRoutes = (from r in context.Routes
select r);

var ignoredRoutes = (from r in allRoutes where r.Ignore == true select r); foreach (var route in ignoredRoutes) { if (route.IsEnabled()) { RouteTable.Routes.IgnoreRoute(route.Pattern); } } var activeRoutes = (from r in allRoutes where r.Ignore == false || r.Ignore == null orderby r.Index descending select r); foreach (var route in activeRoutes) { if (route.IsEnabled()) { MapRoute(context, route); } } }
Code language: JavaScript (javascript)

}
We then take care of our route mapping:

/// /// Maps the given to the route table. ///
/// The used to read the route.
/// The route to map.
private static void MapRoute(DataContext context, BusinessObjects.Route route)
{
var parameters = (from p in context.RouteParameters
where p.Route == route.Id
select p);

var defaults = new RouteValueDictionary(); var constraints = new RouteValueDictionary(); foreach (var param in parameters) { if (!defaults.ContainsKey(param.Name)) { if (param.DefaultValue != null) { if (string.IsNullOrEmpty(param.Type)) { defaults.Add(param.Name, null); } else { defaults.Add(param.Name, GetInstance(param.Type, param.DefaultValue)); } } else { defaults.Add(param.Name, null); } } if (param.Constraint != null) { constraints.Add(param.Name, param.Constraint); } } RouteTable.Routes.Add( route.Key, new System.Web.Routing.Route(route.Pattern, new MvcRouteHandler()) { Defaults = defaults, Constraints = constraints });
Code language: PHP (php)

}
Important note: Don’t use the RouteTable.Routes.MapRoute method when passing in an instance of RouteValueDictionary for defaults and constraints. The MapRoute method provided by MVC creates the dictionaries itself, so you end up with dictionaries within dictionaries, and the routing will fail.

For any parameters that specify a default value and a declared typed, we need to handle the casting of that type. We can use a TypeConverter to handle this, so thats what we’ve done here:

/// /// Gets an instance of the specified type with the given value. ///
/// The typeName for the required type.
/// The value to assign to this type.
/// An instance of the specified type.
private static object GetInstance(string typeName, string value)
{
if (string.IsNullOrEmpty(typeName))
{
throw new ArgumentException(
string.Format(
CultureInfo.CurrentUICulture, Resources.Shared.Exception_ArgumentNullOrEmpty, “typeName”),
“typeName”);
}

Type type = Type.GetType(typeName); if (type == null) { throw new InvalidOperationException( string.Format( CultureInfo.CurrentUICulture, Resources.Shared.Exception_CannotGetType, typeName)); } TypeConverter converter = TypeDescriptor.GetConverter(type); if (converter == null) { throw new InvalidOperationException( string.Format( CultureInfo.CurrentUICulture, Resources.Shared.Exception_NoTypeConverter, type.FullName)); } if (!converter.CanConvertFrom(typeof(string))) { throw new InvalidOperationException( string.Format( CultureInfo.CurrentUICulture, Resources.Shared.Exception_CannotConvertFromString, type.FullName)); } return converter.ConvertTo(value, type);
Code language: JavaScript (javascript)

}
If a valid TypeConverter exists for our target type, we should be able to handle conversion of custom types too.

So our complete application type is as such:

namespace MvcFramework.Web
{
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;
using System.Web.Routing;
using MvcFramework.BusinessObjects;

/// <summary> /// Provides services for Http Applications /// </summary> public class Application : System.Web.HttpApplication { #region Methods /// <summary> /// Registers the configured routes. /// </summary> public void RegisterRoutes() { using (DataContext context = DataContext.CreateDataContext()) { var allRoutes = (from r in context.Routes select r); var ignoredRoutes = (from r in allRoutes where r.Ignore == true select r); foreach (var route in ignoredRoutes) { if (route.IsEnabled()) { RouteTable.Routes.IgnoreRoute(route.Pattern); } } var activeRoutes = (from r in allRoutes where r.Ignore == false || r.Ignore == null orderby r.Index descending select r); foreach (var route in activeRoutes) { if (route.IsEnabled()) { MapRoute(context, route); } } } } /// <summary> /// Refreshes the route table. /// </summary> public void RefreshRoutes() { RouteTable.Routes.Clear(); RegisterRoutes(); } /// <summary> /// Maps the given <see cref="BusinessObjects.Route" /> to the route table. /// </summary> /// <param name="context">The <see cref="DataContext" /> used to read the route.</param> /// <param name="route">The route to map.</param> private static void MapRoute(DataContext context, BusinessObjects.Route route) { var parameters = (from p in context.RouteParameters where p.Route == route.Id select p); var defaults = new RouteValueDictionary(); var constraints = new RouteValueDictionary(); foreach (var param in parameters) { if (!defaults.ContainsKey(param.Name)) { if (param.DefaultValue != null) { if (string.IsNullOrEmpty(param.Type)) { defaults.Add(param.Name, null); } else { defaults.Add(param.Name, GetInstance(param.Type, param.DefaultValue)); } } else { defaults.Add(param.Name, null); } if (param.Constraint != null) { constraints.Add(param.Name, param.Constraint); } } } RouteTable.Routes.Add( route.Key, new System.Web.Routing.Route(route.Pattern, new MvcRouteHandler()) { Defaults = defaults, Constraints = constraints }); } /// <summary> /// Gets an instance of the specified type with the given value. /// </summary> /// <param name="typeName">The typeName for the required type.</param> /// <param name="value">The value to assign to this type.</param> /// <returns>An instance of the specified type.</returns> private static object GetInstance(string typeName, string value) { if (string.IsNullOrEmpty(typeName)) { throw new ArgumentException( string.Format( CultureInfo.CurrentUICulture, Resources.Shared.Exception_ArgumentNullOrEmpty, "typeName"), "typeName"); } Type type = Type.GetType(typeName); if (type == null) { throw new InvalidOperationException( string.Format( CultureInfo.CurrentUICulture, Resources.Shared.Exception_CannotGetType, typeName)); } TypeConverter converter = TypeDescriptor.GetConverter(type); if (converter == null) { throw new InvalidOperationException( string.Format( CultureInfo.CurrentUICulture, Resources.Shared.Exception_NoTypeConverter, type.FullName)); } if (!converter.CanConvertFrom(typeof(string))) { throw new InvalidOperationException( string.Format( CultureInfo.CurrentUICulture, Resources.Shared.Exception_CannotConvertFromString, type.FullName)); } return converter.ConvertTo(value, type); } /// <summary> /// Initialises the application. /// </summary> protected void Application_Start() { RegisterRoutes(); } #endregion }
Code language: HTML, XML (xml)

}
Let me know what you think.

Note (again): Ignore the usage of IsEnabled(), its just an extension method I’ve created for an interface my entity types implement.

Leave a Reply

Your email address will not be published.

Related Post