ASP.Net MVC Handling Segments with Route

A placeholder (such as {category}) acts like a variable – it can contain any value. The framework must be able to understand what the parameters in the URL mean. You can do this one of three ways:

  1. Provide them in a specific order, and for a specific number of segments
  2. Put them in the query string so you have name/value pairs to identify what they are
  3. Make a series of routes with literal segments to provide names to identify what the parameters are

Here is an example of option #3. It is a bit involved compared to using query string parameters, but it is certainly possible as long as you provide some sort of identifier for each route segment.

IEnumerable Extensions

This adds LINQ support for being able to get every possible permutation of parameter values.

using System;
using System.Collections.Generic;
using System.Linq;

public static class IEnumerableExtensions
{
    // Can be used to get all permutations at a certain level
    // Source: http://stackoverflow.com/questions/127704/algorithm-to-return-all-combinations-of-k-elements-from-n#1898744
    public static IEnumerable<IEnumerable<T>> Combinations<T>(this IEnumerable<T> elements, int k)
    {
        return k == 0 ? new[] { new T[0] } :
            elements.SelectMany((e, i) =>
            elements.Skip(i + 1).Combinations(k - 1).Select(c => (new[] { e }).Concat(c)));
    }

    // This one came from: http://stackoverflow.com/questions/774457/combination-generator-in-linq#12012418
    private static IEnumerable<TSource> Prepend<TSource>(this IEnumerable<TSource> source, TSource item)
    {
        if (source == null)
            throw new ArgumentNullException("source");

        yield return item;

        foreach (var element in source)
            yield return element;
    }

    public static IEnumerable<IEnumerable<TSource>> Permutations<TSource>(this IEnumerable<TSource> source)
    {
        if (source == null)
            throw new ArgumentNullException("source");

        var list = source.ToList();

        if (list.Count > 1)
            return from s in list
                   from p in Permutations(list.Take(list.IndexOf(s)).Concat(list.Skip(list.IndexOf(s) + 1)))
                   select p.Prepend(s);

        return new[] { list };
    }
}

RouteCollection Extensions

We extend the MapRoute extension method, adding the ability to add a set of routes to match all possible permutations of the URL.

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Routing;

public static class RouteCollectionExtensions
{
    public static void MapRoute(this RouteCollection routes, string url, object defaults, string[] namespaces, string[] optionalParameters)
    {
        MapRoute(routes, url, defaults, null, namespaces, optionalParameters);
    }

    public static void MapRoute(this RouteCollection routes, string url, object defaults, object constraints, string[] namespaces, string[] optionalParameters)
    {
        if (routes == null)
        {
            throw new ArgumentNullException("routes");
        }
        if (url == null)
        {
            throw new ArgumentNullException("url");
        }
        AddAllRoutePermutations(routes, url, defaults, constraints, namespaces, optionalParameters);
    }

    private static void AddAllRoutePermutations(RouteCollection routes, string url, object defaults, object constraints, string[] namespaces, string[] optionalParameters)
    {
        // Start with the longest routes, then add the shorter ones
        for (int length = optionalParameters.Length; length > 0; length--)
        {
            foreach (var route in GetRoutePermutations(url, defaults, constraints, namespaces, optionalParameters, length))
            {
                routes.Add(route);
            }
        }
    }

    private static IEnumerable<Route> GetRoutePermutations(string url, object defaults, object constraints, string[] namespaces, string[] optionalParameters, int length)
    {
        foreach (var combination in optionalParameters.Combinations(length))
        {
            foreach (var permutation in combination.Permutations())
            {
                yield return GenerateRoute(url, permutation, defaults, constraints, namespaces);
            }
        }
    }

    private static Route GenerateRoute(string url, IEnumerable<string> permutation, object defaults, object constraints, string[] namespaces)
    {
        var newUrl = GenerateUrlPattern(url, permutation);
        var result = new Route(newUrl, new MvcRouteHandler())
        {
            Defaults = CreateRouteValueDictionary(defaults),
            Constraints = CreateRouteValueDictionary(constraints),
            DataTokens = new RouteValueDictionary()
        };
        if ((namespaces != null) && (namespaces.Length > 0))
        {
            result.DataTokens["Namespaces"] = namespaces;
        }

        return result;
    }

    private static string GenerateUrlPattern(string url, IEnumerable<string> permutation)
    {
        string result = url;
        foreach (string param in permutation)
        {
            result += "https://stackoverflow.com/" + param + "/{" + param + "}";
        }

        System.Diagnostics.Debug.WriteLine(result);

        return result;
    }

    private static RouteValueDictionary CreateRouteValueDictionary(object values)
    {
        IDictionary<string, object> dictionary = values as IDictionary<string, object>;
        if (dictionary != null)
        {
            return new RouteValueDictionary(dictionary);
        }
        return new RouteValueDictionary(values);
    }
}

Usage

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            url: "Home/CategoryProducts", 
            defaults: new { controller = "Home", action = "CategoryProducts" }, 
            namespaces: null, 
            optionalParameters: new string[] { "category", "manufacturer", "attribute" });

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

This adds a complete set of routes to match the URL patterns:

Home/CategoryProducts/category/{category}/manufacturer/{manufacturer}/attribute/{attribute}
Home/CategoryProducts/category/{category}/attribute/{attribute}/manufacturer/{manufacturer}
Home/CategoryProducts/manufacturer/{manufacturer}/category/{category}/attribute/{attribute}
Home/CategoryProducts/manufacturer/{manufacturer}/attribute/{attribute}/category/{category}
Home/CategoryProducts/attribute/{attribute}/category/{category}/manufacturer/{manufacturer}
Home/CategoryProducts/attribute/{attribute}/manufacturer/{manufacturer}/category/{category}
Home/CategoryProducts/category/{category}/manufacturer/{manufacturer}
Home/CategoryProducts/manufacturer/{manufacturer}/category/{category}
Home/CategoryProducts/category/{category}/attribute/{attribute}
Home/CategoryProducts/attribute/{attribute}/category/{category}
Home/CategoryProducts/manufacturer/{manufacturer}/attribute/{attribute}
Home/CategoryProducts/attribute/{attribute}/manufacturer/{manufacturer}
Home/CategoryProducts/category/{category}
Home/CategoryProducts/manufacturer/{manufacturer}
Home/CategoryProducts/attribute/{attribute}

Now when you use the following URL:

Home/CategoryProducts/category/c_50_ShowcasesDisplays

The action CategoryProducts on the HomeController will be called. Your category parameter value will be c_50_ShowcasesDisplays.

It will also build the corresponding URL when you use ActionLink, RouteLink, Url.Action, or UrlHelper.

@Html.ActionLink("ShowcasesDisplays", "CategoryProducts", "Home", 
    new { category = "c_50_ShowcasesDisplays" }, null)

// Generates URL /Home/CategoryProducts/category/c_50_ShowcasesDisplays

Leave a Comment