Wednesday, October 7, 2026

ASP.NET Core UseRouting / UseEndpoints explained

ASP.NET Core engine is capable of using so called endpoints. The idea is to use the list of your endpoint paths to build a DFA matcher that matches endpoints as fast as possible, no matter if you have 5 or 5000 endpoints.

The official documentation explains how routes are defined.

The problem of understanding of how internals work is that there are two canonical calls, UseRouting and UseEndpoints and this has been simplified to MapXXX in Minimal API (where each call to Map actually registers an endpoint.

The example below demonstrates and explains when an endpoint is registered, when it's matched and what if you want to have routes that cannot be defined using the language but you just want to have a complete freedom of matching any Func<string, bool> to a request delegate.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

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

// Routing middleware sets the endpoint for current request
// If any other later middleware calls .GetEndpoint(),
// the selected endpoint will be available
app.UseRouting();

// An example middleware that can access the endpoint data
app.Use(async (context, next) =>
{
    Endpoint? matchedEndpoint = context.GetEndpoint();

    if (matchedEndpoint != null)
    {
        Console.WriteLine($"[log] matched endpoint: {matchedEndpoint.DisplayName}");

        // endpoint can have additional metadata
        var securityMetadata = matchedEndpoint.Metadata.GetMetadata<AccessLevelRestriction>();
        if (securityMetadata != null)
        {
            Console.WriteLine($"[log] Required access level: {securityMetadata.Level}");
        }
    }
    else
    {
        Console.WriteLine("[log] no endpoint matched for current url");
    }

    // go next, to UseEndpoints
    await next();
});

// Endpoint table definition
// UseEndpoints does two things
// * when called on app (app.UseEndpoints) once, at app start
//   it configures a list of known endpoints and their RequestDelegates
// * it also registers a middleware that runs at each request
//   and calls the RequestDelegate whenever a route is matched in current
//   request (by the middleware from UseRouting!)
#pragma warning disable ASP0014
app.UseEndpoints(endpoints =>
{
    // map path to RequestDelegate 
    endpoints.Map("/profile/{userId}", async context =>
    {
        // route matched by UseRouting left its RouteValues 
        var userId = context.Request.RouteValues["userId"];

        context.Response.ContentType = "text/plain; charset=utf-8";
        await context.Response.WriteAsync($"Welcome to the profile of a user ID: {userId}");
    })
    .WithDisplayName("UseProfileEndpoint")               // set name
    .WithMetadata(new AccessLevelRestriction(Level: 4)); // attach metadata
});

// ASP.NET uses a template language to define routes
// this language is optimized so that the DFA matcher is built to match routes as fast as possible
// (finite state automata)
// this template language allows fixed segments, slashes, curly brackets, variables, type constraints
// but it's not a mapping from Func<string, bool> to RequestDelegates
// there are definitely routes you can't match!
// But, a custom matcher from Func<string, bool> to RequestDelegates can be easily built
// Here is an example usage:
app.MapCustom(
    path => path.StartsWith("/cms", StringComparison.OrdinalIgnoreCase),
    async context =>
    {
        // no endpoint here, it's our custom matcher
        await context.Response.WriteAsync("The custom matcher matched this one!");
    });

// If neither an endpoint nor a custom route matched - default delegate
app.Run(async context =>
{
    context.Response.StatusCode = 404;
    await context.Response.WriteAsync("404 - nothing matched.");
});

app.Run();

public record AccessLevelRestriction(int Level);

public static class SimpleCustomMappingExtensions
{
    // The custom mapper maps Func<string, bool> to RequestDelegate
    public static IApplicationBuilder MapCustom(
        this IApplicationBuilder app,
        Func<string, bool> urlMatcher,
        RequestDelegate requestDelegate)
    {
        // just a middleware
        return app.Use(async (context, next) =>
        {
            var path = context.Request.Path.Value ?? string.Empty;

            // does it match
            if (urlMatcher(path))
            {
                // call the delegate and stop
                await requestDelegate(context);
                return;
            }
            else
            {
                // call next delegate
                await next();
            }
        });
    }
}

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.

Tuesday, October 6, 2026

Modern way of using the Application Builder in a console app

It's just:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var settings = new HostApplicationBuilderSettings() { Args = args };
var builder = Host.CreateEmptyApplicationBuilder( settings );

builder.Configuration
    .SetBasePath(AppContext.BaseDirectory)
    .AddJsonFile("appsettings.json");

builder.Services.AddTransient<AppRunner>();

builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Trace);

var app = builder.Build();

//app.Run();
var runner = app.Services.GetRequiredService<AppRunner>();
runner.Run();

public class AppRunner( ILogger<AppRunner> logger, IConfiguration cfg)
{
    public void Run()
    {
        var foo = cfg["Foo"];
        logger.LogDebug($"Hello world {foo}");
    }
}

with simple appsettings.json:

{
   "Foo":  "Bar"
}

Friday, September 18, 2026

AI is like calculators?

Someone presented an old photo of math teachers protesting against the introduction of calculators. There was a caption under the photo, "AI. We've been there before".

Well. My comment was that things are similar and analogies work. Unless they don't. In particular, we don't really know what the impact of AI will be decades from now but we already know that calculators didn't change that much. Just to keep in mind things that were introduced, used for years despite initial controversies but ultimately we discarded them as dangerous, like leaded gas or asbestos.

We shouldn't fall into the premature assessment fallacy. We have arguments for and against AI but we don't really know what future it holds.

Monday, September 14, 2026

Hobby, piano sheets

Musescore got a nice feature that allows exporting your scores to MP4s. Thus, I can share my old piano works as YT videos.

Enjoy

Sunday, August 30, 2026

Android disaster with Google Play

New Samsung phone, I'm helping someone install everything. Everything is going well, apps are updating. WhatsApp is transferring data. I uninstall several unwanted apps, including Outlook and LinkedIn. I restart. The Play Store stops launching.

This is quite serious; it can't be that the Play Store isn't working. I'm trying the standard method, clearing the memory and data for the Play Store and Google Services, and restarting.

Nothing. Gemini suggests I should install APK manually. It must be kidding.

Two or three restarts later, I notice that I get a prompt that asks if I want to restore Outlook and LinkedIn. I discarded this prompt twice but this time I decide to give it a try.

Play store notification appears and it shows it's installing the two apps.

A minute later, the notification disappears and I try Play Store once again.

It works.

It's utterly shameful that uninstalling two third-party apps can break such fundamental system service as Google Play. I don't care whose fault this is, Google, Samsung and its Knox.

It should never, ever happen. Shame on you.

Monday, August 10, 2026

Functional programming in JavaScript (12)

Please take a look at other posts about functional programming in JavaScript:

  1. Part 1 - what Functional Programming is about
  2. Part 2 - Functional Pipelines
  3. Part 3 - the Y Combinator
  4. Part 4 - Monads
  5. Part 5 - Trampoline
  6. Part 6 - Lenses
  7. Part 7 - Church encoding (booleans)
  8. Part 8 - Church encoding (arithmetics)
  9. Part 9 - Church encoding (if, recursion)
  10. Part 10 - Church encoding (lists)
  11. Part 11 - Church encoding (strings, objects)
  12. Part 12 - modularity

Modularity lets us create code that spans multiple modules (technically: files).

But how it works under the hood? How do import and export actually work in a functional way? How it is possible to have cyclic dependencies between modules?

We are going to build a short example of three modules: A, B and Main. Modules A and B will depend on each other.

Let's start with the code:

// ==========================================
// Infrastructure
// ==========================================
const registry = {};

function registerModule(id, factoryFn) {
    registry[id] = {
        id,
        factoryFn,
        exports: {},        
        isEvaluated: false  
    };
}

function evaluateModule(id) {
    const mod = registry[id];
    
    if (mod.isEvaluated) {
        return mod.exports;
    }

    mod.isEvaluated = true;

    const _import = (dependencyName) => {
        return evaluateModule(dependencyName);
    };

    const _export = (name, fn) => {
        mod.exports[name] = fn;
    };

    mod.factoryFn(_import, _export);

    return mod.exports;
}

// ==========================================
// Example use
// ==========================================

// Module A (a.js) - cyclic dependency to B
registerModule('./a.js', (_import, _export) => {
    const _b = _import('./b.js');

    let valueA = "Wartość z A";

    _export('getA', () => valueA);
    _export('callB', () => "A calls B -> " + _b.getB() );
});

// Module B (b.js) - cyclic dependency to A
registerModule('./b.js', (_import, _export) => {
    const _a = _import('./a.js');

    let valueB = "Wartość z B";

    _export('getB', () => valueB);
    _export('callA', () => "B calls A -> " + _a.getA() );
});

// main module (main.js)
registerModule('./main.js', (_import, _export) => {
    const _a = _import('./a.js');
    const _b = _import('./b.js');

    console.log(_a.callB());
    console.log(_b.callA());
});

evaluateModule('./main.js');

Look how simple it is. We have a global registry of modules. Our register function just adds a module to the registry.

The only non trivial function here is the evaluation function. The function executes module's factory function.

Whenever we execute the import, we just recursively call the evaluation function. And this is where a problem with cyclic dependencies could possibly occur.

How do we prevent it?

    ...
    if (mod.isEvaluated) {
        return mod.exports;
    }

    mod.isEvaluated = true;

We check if module is already evaluated and if it is, we just return its exports - but the actual list of module's exports can possibly be empty at this point! In fact, the list of module's exports can be available only after the module initialization is complete!

That's why we only export functions and we call the evaluation of the main module only when the dependency graph is fully evaluated (all exports are available). If a module is imported multiple times (by other modules), its factory function is evaluated only once, all subsequent initializations terminate early.