var Matt = new Hero();

C#, ASP.NET MVC, MEF, Javascript…and anything else that interests me.

Matthew Abbott On December - 23 - 2009

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.

/// <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);
            }
        }
    }
}

We then take care of our route mapping:

/// <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
                   });
}

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:

/// <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);
}

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
    }
}

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.

Digg This
Reddit This
Stumble Now!
Buzz This
Vote on DZone
Share on Facebook
Bookmark this on Delicious
Kick It on DotNetKicks.com
Shout it
Share on LinkedIn
Bookmark this on Technorati
Post on Twitter
Google Buzz (aka. Google Reader)
Categories: .NET, Development

Leave a Reply

You must be logged in to post a comment.

Me

Featured Posts

The Magic of IoC

I was bored…
…so I decided to design an IoC Container! This actually turned out to be a lot of fun too. I’d like to firstly state that a) there is no reason for this code, and b) this is not a replacement for established IoC solutions, such as Unity, Castle Windsor, Ninject, Authofac, StructureMap, and [...]

Integrating Regula with ASP.NET MVC and DataAnnotations

Previously I discovered Regula, an annotation-based validation framework used for client-side validation of form elements. I wondered if it was possible to automatically wire up client-side validation using DataAnnotation’s ValidationAttributes on the server side, very similar to how xVal might handle it.
This is by no means usable, its really an experiment to see what [...]

Regula as a jQuery Plugin

Earlier this evening I came across a Stack Overflow question regarding a most excellent new javascript validation framework, called Regula. Regula is an annotation-based validation framework that hides the complexities of binding complex validation events to controls by extending the DOM element itself with annotations about how it should be validated. Here is a [...]

Modular ASP.NET MVC using the Managed Extensibility Framework (MEF), Part Three

See also: Modular ASP.NET MVC using the Managed Extensibility Framework (MEF), Part One
See also: Modular ASP.NET MVC using the Managed Extensibility Framework (MEF), Part Two
Currently reading: Modular ASP.NET MVC using the Managed Extensibility Framework (MEF), Part Three

Firstly, sorry it’s been so long since my last MVC+MEF post, been a bit busy with life in general. [...]

Javascript Linq Extensions

I love Linq. Linq just makes code awesome. Well, that’s my view on it anyway. I also like Javascript, and through libraries like JQuery we’ve now got a framework whereby you can pretty much do anything, but often I like getting down to the bare-bones of Javascript and just having a go at what [...]

Search my site

Twitter Updates

Links