Thursday, October 8, 2026

ASP.NET Core custom EndpointFilters vs custom Route Constraints

Before we dig into the topic, let's just peek into the EndpointDataSource collection which groups routes and can be examined at runtime:

app.MapGet( "/debug/endpoints", 
    async ( IEnumerable<EndpointDataSource> dataSources ) =>
{
    var list = new List<object>();

    foreach ( var dataSource in dataSources )
    {
        foreach ( var endpoint in dataSource.Endpoints )
        {
            if ( endpoint is RouteEndpoint routeEndpoint )
            {
                // Wyciągamy metadane (np. nasz WymaganyPoziomDostepu, HttpMethodMetadata itp.)
                var httpMethodMeta = endpoint.Metadata.GetMetadata<HttpMethodMetadata>();
                var methods        = httpMethodMeta?.HttpMethods != null
                    ? string.Join(", ", httpMethodMeta.HttpMethods)
                    : "ANY";

                list.Add( new
                {
                    DisplayName   = endpoint.DisplayName,
                    RoutePattern  = routeEndpoint.RoutePattern.RawText,
                    Methods       = methods,
                    MetadataCount = endpoint.Metadata.Count
                } );
            }
        }
    }

    return Results.Ok( list );
} );

Now, both Endpoint Filters and Route Constrains can be used to add constraints to matched routes.

An Endpoint Filter is attached to the endpoint and acts as kind of a "wrapper", it can add custom filtering, call the actual request delegate and modify the result:

app.MapGet( "/rf/{id:int}", (int id) => $"rf: {id}" ).AddEndpointFilter( async ( context, next ) =>
{
    var id = context.GetArgument<int>(0);
    if ( id < 10 )
    {
        return Results.BadRequest();
    }

    Console.WriteLine( "before request delegate call" );

    // call actual delegate
    var result = await next( context );

    Console.WriteLine( "after request delegate call" );

    // you can modify the result
    return result;
} );

On the other hand, a custom Route Constraint can add a finetuned filtering to route parameters:

public class MyCustomConstraint : IRouteConstraint
{
    public bool Match(
        HttpContext? httpContext,
        IRouter? route,
        string routeKey,
        RouteValueDictionary values,
        RouteDirection routeDirection )
    {
        if ( values.TryGetValue( routeKey, out var value ) && value is string stringValue )
        {
            // accept only strings starting with "cust" and longer than 5 chars
            return stringValue.StartsWith( "cust" ) && stringValue.Length > 5;
        }

        return false; 
    }
}

...

builder.Services.Configure<RouteOptions>( options =>
{
    options.ConstraintMap.Add( "customconstraint", typeof( MyCustomConstraint ) );
} );

...

app.MapGet( "/cc/{id:customconstraint}", ( string id ) => $"cc: {id}" );
Route Constraint Endpoint Filter
What for? Decision: match this endpoint Action: custom code around the endpoint
When? Early, decides which endpoint should be selected Late, endpoint is already selected
Scope? Tiny, only specific segment in the route template like {id:customconstraint} Whole endpoint
Has access to what? Only raw url parameter values Full access to HttpContext, arguments and the result
Does what? Returns true/false - match the endpoint or skip Can modify the result, skip processing and/or add custom code before and after

No comments: