Endpoint Routing in ASP.NET Core

Endpoint routing is the mechanism ASP.NET Core uses to match incoming HTTP requests to the code that handles them. Introduced as a first-class concept in ASP.NET Core 3.0, it decouples the act of matching a route from executing the handler, allowing middleware between those two steps to make decisions based on the matched endpoint.

The Two-Phase Model

Endpoint routing works in two phases:

  1. Routing (UseRouting) — examines the request URL and selects the best matching endpoint.
  2. Endpoint execution (MapControllers, MapGet, etc.) — runs the selected endpoint's delegate.

Middleware registered between these two phases can inspect the selected endpoint before it executes:

Example.cs
app.UseRouting();

// This middleware knows which endpoint was selected
app.Use(async (context, next) =>
{
    var endpoint = context.GetEndpoint();
    if (endpoint is not null)
    {
        Console.WriteLine($"Matched endpoint: {endpoint.DisplayName}");
    }
    await next(context);
});

app.UseAuthorization(); // Uses endpoint metadata to check policies

app.MapControllers();

This is how authorisation works — it reads the [Authorize] attribute from the matched endpoint's metadata and enforces the policy before the handler runs.

Route Templates

Route templates define the URL pattern an endpoint matches. They support literal segments, parameters, constraints, and catch-all parameters:

Example.cs
// Literal segment
app.MapGet("/products", () => "All products");

// Route parameter
app.MapGet("/products/{id}", (int id) => $"Product {id}");

// Optional parameter
app.MapGet("/products/{id?}", (int? id) =>
    id.HasValue ? $"Product {id}" : "All products");

// Constrained parameter
app.MapGet("/products/{id:int}", (int id) => $"Product {id}");

// Catch-all parameter
app.MapGet("/files/{**path}", (string path) => $"File: {path}");

Route Constraints

Constraints restrict which values a route parameter will match:

Example.cs
// Only matches integers
app.MapGet("/orders/{id:int}", (int id) => $"Order {id}");

// Only matches GUIDs
app.MapGet("/users/{userId:guid}", (Guid userId) => $"User {userId}");

// Minimum value
app.MapGet("/page/{number:min(1)}", (int number) => $"Page {number}");

// Regex constraint
app.MapGet("/slugs/{slug:regex(^[a-z0-9-]+$)}", (string slug) =>
    $"Slug: {slug}");

// Multiple constraints
app.MapGet("/items/{id:int:min(1):max(1000)}", (int id) =>
    $"Item {id}");

If a request matches the URL pattern but fails a constraint, routing treats it as if the route did not match at all — the framework continues looking for other matches.

Route Groups

.NET 7 introduced route groups to reduce repetition when several endpoints share a common prefix or set of filters:

Example.cs
var api = app.MapGroup("/api");
var v1 = api.MapGroup("/v1").AddEndpointFilter<ApiVersionFilter>();

v1.MapGet("/products", GetProducts);
v1.MapGet("/products/{id}", GetProduct);
v1.MapPost("/products", CreateProduct);

// Results in: /api/v1/products, /api/v1/products/{id}

Groups can apply filters, metadata, and authorisation policies to all contained endpoints at once.

Endpoint Metadata

Every endpoint carries metadata — attributes, tags, and other data that middleware can inspect:

Example.cs
app.MapGet("/admin/dashboard", () => "Admin Dashboard")
    .RequireAuthorization("AdminPolicy")
    .WithTags("admin")
    .WithName("AdminDashboard")
    .WithDescription("Main admin dashboard");

You can also read custom metadata in middleware:

Example.cs
app.Use(async (context, next) =>
{
    var endpoint = context.GetEndpoint();
    var requiresAudit = endpoint?.Metadata
        .GetMetadata<AuditAttribute>() is not null;

    if (requiresAudit)
    {
        // Log the access for audit trail
    }

    await next(context);
});

Host Matching

Endpoints can be constrained to specific hosts, which is useful for multi-tenant applications:

Example.cs
app.MapGet("/", () => "Public site")
    .RequireHost("www.example.com");

app.MapGet("/", () => "Admin portal")
    .RequireHost("admin.example.com");

Route Precedence

When multiple routes could match a request, ASP.NET Core uses precedence rules:

  1. Literal segments are preferred over parameters.
  2. Constrained parameters are preferred over unconstrained ones.
  3. More specific templates win over less specific ones.
  4. Catch-all parameters have the lowest precedence.

If two routes have equal precedence and both match, the framework throws an AmbiguousMatchException at runtime.

Key Takeaways

Endpoint routing's two-phase model is what makes ASP.NET Core's middleware pipeline so flexible. Route templates, constraints, and groups give you fine-grained control over URL matching. Understanding precedence rules helps you avoid ambiguous matches, and endpoint metadata enables middleware to make decisions based on the selected endpoint before it executes.