Using Parameter Transformers To Modify Route And Parameter Values

Available from ASP.NET Core 2.2, parameter transformers enable the centralisation of logic for modifying the value of routes and route parameters in URLs generated by the anchor tag helper or the Url helper.

There are two cases where parameter transformers can be used:

  1. To modify the route that the framework generates automatically from the location of a Razor Page
  2. To modify a parameter value in a route

Modifying Page Routes

By default, the route for a particular page is constructed from its file location and name. For example, a Razor page located at /Pages/SalesReports/RevenueReport.cshtml will have a route template of SalesReports/RevenueReport generated for it. Let's say that you prefer to have the page reachable at sales-reports/revenue-report instead. You could apply an override route, but if you have multiple pages to override in the same manner, this approach can be time-consuming and brittle. Instead, you can create a parameter transformer to manage the process for all pages in the application.

A parameter transformer is a class that implements IOutboundParameterTransformer. This interface has one method, TransformOutbound which returns a string. The body of the method performs the transformation:

using Microsoft.AspNetCore.Routing;
using System.Text.RegularExpressions;

public class HyphenateRouteParameterTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        if (value == null) 
        { 
            return null; 
        }
        return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
    }
}

In this simple example, the regular expression identifies separate words in a Pascal case string. It looks for instances in the string - the part of the route's default template that relates to the file path of the page - where an upper case letter follows a lower case letter (i.e. where a new word starts) and then inserts a hyphen between them. The resulting string is then converted to lower case and returned, replacing the default template for the route.

To get this to work, the parameter transformer needs to be registered in Startup:

services.AddRazorPages().AddRazorPagesOptions(options =>
{
    options.Conventions.Add(new PageRouteTransformerConvention(new HyphenateRouteParameterTransformer()));
});
services.AddMvc().AddRazorPagesOptions(options =>
{
    options.Conventions.Add(new PageRouteTransformerConvention(new HyphenateRouteParameterTransformer()));
});

The asp-page attribute on the anchor tag helper still takes the original file name and path:

<a asp-page="salesreports/revenuereport">Revenue Report</a>

This is converted to the correct template at runtime:

<a href="/sales-reports/revenue-report">Revenue Report</a>

Modifying Parameter Values

An obvious application for this feature is modifying route parameter values obtained dynamically from a database or similar, so that the resulting URL is SEO-friendly. The parameter value can be anything - the title of a news story, a product name and so on. In this case, the parameter transformer is registered and used in the same way as a custom constraint.

The following illustration features an extension method, borrowed from the answer to a Stackoverflow question: How does Stack Overflow generate its SEO-friendly URLs?

using System.Text; 

public static class Extensions
{
    public static string ToSlug(this string value, bool toLower = true)
    {
        if (value == null)
            return "";

        var normalised = value.Normalize(NormalizationForm.FormKD);

        const int maxlen = 80;
        int len = normalised.Length;
        bool prevDash = false;
        var sb = new StringBuilder(len);
        char c;

        for (int i = 0; i < len; i++)
        {
            c = normalised[i];
            if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
            {
                if (prevDash)
                {
                    sb.Append('-');
                    prevDash = false;
                }
                sb.Append(c);
            }
            else if (c >= 'A' && c <= 'Z')
            {
                if (prevDash)
                {
                    sb.Append('-');
                    prevDash = false;
                }
                // Tricky way to convert to lowercase
                if (toLower)
                    sb.Append((char)(c | 32));
                else
                    sb.Append(c);
            }
            else if (c == ' ' || c == ',' || c == '.' || c == '/' || c == '\\' || c == '-' || c == '_' || c == '=')
            {
                if (!prevDash && sb.Length > 0)
                {
                    prevDash = true;
                }
            }
            else
            {
                string swap = ConvertEdgeCases(c, toLower);

                if (swap != null)
                {
                    if (prevDash)
                    {
                        sb.Append('-');
                        prevDash = false;
                    }
                    sb.Append(swap);
                }
            }

            if (sb.Length == maxlen)
                break;
        }
        return sb.ToString();
    }

    static string ConvertEdgeCases(char c, bool toLower)
    {
        string swap = null;
        switch (c)
        {
            case 'ı':
                swap = "i";
                break;
            case 'ł':
                swap = "l";
                break;
            case 'Ł':
                swap = toLower ? "l" : "L";
                break;
            case 'đ':
                swap = "d";
                break;
            case 'ß':
                swap = "ss";
                break;
            case 'ø':
                swap = "o";
                break;
            case 'Þ':
                swap = "th";
                break;
        }
        return swap;
    }
}

This is a pretty robust way to convert characters in any string to their ASCII equivalents so that they are safe to use in a URL, and then insert hyphens between words. If you want to use the ToSlug method prior to ASP.NET Core 2.2, you can do so in an anchor tag helper:

<a asp-page="product" asp-route-name="@Model.ProductName.ToSlug()">@Model.ProductName</a>

Rather than remembering to use this method in all places throughout the application where it is needed, you can instead create a parameter transformer that uses it:

public class SlugParameterTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        return value?.ToString().ToSlug();
    }
}

This is registered in the ConfigureServices method:

services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add("slug", typeof(SlugParameterTransformer));
});

It is added to the ConstraintMap in the same way as a custom constraint. However, it is not a constraint. But you add it to the route template as part of the @page directive in the same way as a constraint:

@page "{name:slug}"

Now, there is no need to use the ToSlug method on the value provided to the name route parameter value in anchor tags. The parameter transformer calls it instead:

<a asp-page="product" asp-route-name="@Model.ProductName">@Model.ProductName</a>

Assuming that the the value represented by Model.ProductName is "Original Frankfurter grüne Soße" ,the generated output for the anchor tag helper is:

<a href="/product/original-frankfurter-grune-sosse">Original Frankfurter grüne Soße</a>

Summary

Parameter transformers implement IOutboundParameterTransformer and can act on two parts of the route: those added as a PageRouteTransformerConvention act on the part of the route generated from the location of the page. Those added to the ConstraintMap act on specific route parameters.

Last updated: 14/02/2020 07:41:06

© 2018 - 2024 - Mike Brind.
All rights reserved.
Contact me at Outlook.com