.NET Core MVC: regex routing with named groups

Routing with regular expressions... to some the world would be a better place if everyone understood regular expressions. To others the world would be a better place without regular expressions at all. The thing is, they're easy to create, but hard to maintain. And every language has its own take on regex.
One of the big advantages of the .Net implementation is named groups.  Today I want to show how to leverage named regular expression groups to build a routing constraint that will map each group value to a named route value.

Friendly urls

At wehkamp we use the following friendly URLs to build a SEO-friendly navigation structure:

TypeUrl
Category/speelgoed-games/lego/C25
Shop/speelgoed-games/lego/C25_K58
Segment/lego/lego-architecture/C25_K58_LAR
Product/speelgoed-games/lego/lego-architecture/lego-architecture-architecture-new-york-21028/C25_K58_LAR_653948/

Let's assume we need to map these routing values into a parameter object:

class Parameter
{
    [FromRoute]
    public string CategoryCode { get; set; }
    [FromRoute]
    public string ShopCode { get; set; }
    [FromRoute]
    public string SegmentCode { get; set; }
    [FromRoute]
    public string ProductId { get; set; }
}

Introducing a regex routing constraint

Routing constraints can be used to extract routing values from a URL. We could build a single constraint for every "type" of URL. Or... we could build a more generic constraint that will take an expression and use it as a source for parsing the routing values.

Here's our attempt:

public class RegexNamedGroupRoutingConstraint : IRouteConstraint
{
    private readonly List<Regex> regexes = new List<Regex>();

    public RegexNamedGroupRoutingConstraint(params string[] regexes)
    {
        foreach (var regex in regexes)
        {
            this.regexes.Add(new Regex(regex));
        }
    }

    public bool Match(HttpContext httpContext,
                      IRouter route,
                      string routeKey,
                      RouteValueDictionary values,
                      RouteDirection routeDirection)
    {
        if (values[routeKey] == null)
        {
            return false;
        }

        var url = values[routeKey].ToString();

        foreach (var regex in regexes)
        {
            var match = regex.Match(url);
            if (match.Success)
            {
                foreach (Group group in match.Groups)
                {
                    values.Add(group.Name, group.Value);
                }

                return true;
            }
        }

        return false;
    }
}

Note: we support multiple expression just to make things more readable. What we try to do can be done by a single expression, but it won't be very readable as it uses a lot of optional groups.

Add it to the startup

Let's hook it up in the Startup class by adding the route to the app.UseMvc:

app.UseMvc(routes =>
{
    var shopRoutingConstraint = new RegexNamedGroupRoutingConstraint(
    "/(?<CategoryCode>[A-Z0-9]{3})_(?<ShopCode>[A-Z0-9]{3})_(?<SegmentCode>[A-Z0-9]{3})_(?<ProductId>\d+)/?$",
    "/(?<CategoryCode>[A-Z0-9]{3})_(?<ShopCode>[A-Z0-9]{3})_(?<SegmentCode>[A-Z0-9]{3})/?$",
    "/(?<CategoryCode>[A-Z0-9]{3})_(?<ShopCode>[A-Z0-9]{3})/?$",
    "/(?<CategoryCode>[A-Z0-9]{3})/?$");

    routes.MapRoute(
        name: "nav-by-codes",
        template: "{*url}",
        constraints: new { url = shopRoutingConstraint },
        defaults: new { controller = "Shop", action = "Detail" });
});

The important part is the {*url} template. This will use a catch-all for the route. This will feed the path of the URL into the constraint (not the host nor the query-string).

Summary

Regular expressions can be very useful in mapping routing values. The .Net routing engine will do the actual mapping. In this way you can build a nice routing convention with the FromRoute decoration on parameters and / or properties.

expand_less