Wednesday, October 7, 2026

ASP.NET Core custom middleware for catching CMS-like paths

This tiny ASP.NET Core example shows a nice simplification in the new routing model.

In old ASP.NET Framework, a custom route could be added to the pipeline. Such custom route could parse incoming paths and leave some extra info in the RouteData dictionary. This extra info could then be used by a custom handler or the MVC handler.

In the new model, this extra info could come from just another middleware added to the pipeline.

An example. Suppose you build a CMS-like app and you want your routing to handle following cases:

  • /CMS/section/info.html - a CMS request, site section, page name: info
  • /CMS/section/subsection/info.html - a CMS request, site section/subsection, page name: info
  • /CMS/section/subsection - a CMS request, site section/subsection, page name: index (a default page name in a section)
  • /whateverelse - not a CMS page

A simple middleware would parse incoming paths, store the extra info and let middlewares that are later in the pipeline use this extra info:

using Microsoft.AspNetCore.Http.Extensions;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseCustomRouting();
app.Run( async context =>
{
    var sitename = context.Request.RouteValues[CustomRoutingMiddleware.SITENAME];
    var pagename = context.Request.RouteValues[CustomRoutingMiddleware.PAGENAME];

    if ( sitename != null && pagename != null )
    {
        await context.Response.WriteAsync( $"cms, site {sitename}, page {pagename}" );
    }
    else
    {
        await context.Response.WriteAsync( $"not cms, path {context.Request.GetDisplayUrl()}" );
    }
} );

app.Run();

public class CustomRoutingMiddleware
{
    public const string CMS = "CMS";
    public const string SITENAME = "siteName";
    public const string PAGENAME = "pageName";

    public const string DEFAULTPAGEEXTENSION = ".html";

    private readonly RequestDelegate _next;

    public CustomRoutingMiddleware( RequestDelegate next )
    {
        _next = next;
    }

    public async Task Invoke( HttpContext context )
    {
        var virtualPath = context.Request.Path.ToString();

        var segments = virtualPath.ToLower().Split( 
            new[] { '/' }, 
            StringSplitOptions.RemoveEmptyEntries );

        if ( segments.Length >= 1 && 
             string.Equals( segments.First(), CMS, StringComparison.InvariantCultureIgnoreCase ) )
        {
            if ( segments.Last().IndexOf( DEFAULTPAGEEXTENSION ) > 0 )
            {
                context.Request.RouteValues[SITENAME] = 
                    string.Join( "/", segments.Skip( 1 ).Take( segments.Length - 2 ).ToArray() );
                context.Request.RouteValues[PAGENAME] = 
                    segments.Last().Substring( 0, segments.Last().IndexOf( "." ) );
            }
            else if ( segments.Last().IndexOf( "." ) < 0 )
            {
                context.Request.RouteValues[SITENAME] = 
                    string.Join( "/", segments.Skip( 1 ).ToArray() );
                context.Request.RouteValues[PAGENAME] = 
                    "index.html";
            }
        }

        await _next( context );
    }
}

public static class CustomRoutingMiddlewareExtensions
{
    public static IApplicationBuilder UseCustomRouting( this IApplicationBuilder builder )
    {
        return builder.UseMiddleware<CustomRoutingMiddleware>();
    }
}

This is really concise and ellegant. I really like the the lightweight model of middlewares comparing to the old model of route handlers, modules and handlers.

No comments: