Skip to content

[API Review]: Add support for endpoint filters in minimal APIs #40506

@captainsafia

Description

@captainsafia

The implementation for this feature exists in #40491.

We add a new IRouteHandlerFilter interface that users will implement when writing their own filters.

public interface IRouteHandlerFilter
{
  ValueTask<object?> InvokeAsync(RouteHandlerFilterContext context, Func<RouteHandlerFilterContext, ValueTask<object?>> next)
}

The RouteHandlerFilterContext captures the HttpContext and the list of parameters provided to a route handler for access in the filter.

Note: we want to users to be able to modify the existing parameters that are provided to the handler in their filters which is why the Parameters property is typed as IList and IReadOnlyList. Although IReadOnlyList has the benefit of limiting additions/removals from the list, it also prohibits mutations of existing items (e.g. CS0200 galore). However, we anticipate that we will make an incremental improvement to Parameters and type it as a variadic generic object that provides information about the type, parameter name, and validity of a parameter so the question of IList vs IReadOnlyList is minute at the moment.

public class RouteHandlerFilterContext
{
    public RouteHandlerFilterContext(HttpContext httpContext, params object[] parameters)
    {
        HttpContext = httpContext;
        Parameters = parameters;
    }

    public HttpContext HttpContext { get; }

    public IList<object?> Parameters { get; }
}

We add a new RouteHandlerFilters field to RequestDelegateFactoryOptions that can be used to pass the list of filters registered on a handler to the code-gen in the RequestDelegateFactory.

public class RequestDelegateFactoryOptions
{
    public IEnumerable<IRouteHandlerFilter>? RouteHandlerFilters { get; init; }
}

We add a collection of new extension methods that users can invoke on their endpoints to register handlers.

public static class RouteHandlerFilterExtensions
{
    public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, IRouteHandlerFilter filter) { }

    public static RouteHandlerBuilder AddFilter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFilterType>(this RouteHandlerBuilder builder) where TFilterType : IRouteHandlerFilter, new() { }

    public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, Func<RouteHandlerFilterContext, Func<RouteHandlerFilterContext, ValueTask<object?>>, ValueTask<object?>> routeHandlerFilter) { }
}

Code Sample

string SayHello(string name, int age, DateOnly birthDate) => $"Hello, {name}! You are {age} years old and born in {birthDate.Year} on a {birthDate.DayOfWeek}.";
app.MapGet("/hello/{name}/{age}/{birthDate}", SayHello)
    .AddFilter(async (RouteHandlerFilterContext context, Func<RouteHandlerFilterContext, ValueTask<object>> next) =>
    {
        var age = (int)context.Parameters[1];
        var birthDate = (DateOnly)context.Parameters[2];
        var expectedAge = DateTime.Now.Year - birthDate.Year;
        // if (age != expectedAge)
        if (context.HttpContext.Response.StatusCode == 400)
        {
            return Results.Problem($"Age of {age} does not match birthdate.");
        }
        return await next(context);
    })
    .AddFilter<UpdateDateFilter>()
    .AddFilter(new AddLastNameFilter("Abdalla"));

app.Run();

public class UpdateDateFilter : IRouteHandlerFilter
{
    public async ValueTask<object> InvokeAsync(RouteHandlerFilterContext context, Func<RouteHandlerFilterContext, ValueTask<object>> next)
    {
        var date = (DateOnly)context.Parameters[2];
        context.Parameters[2] = date.AddYears(2);
        return await next(context);
    }
}

public class AddLastNameFilter : IRouteHandlerFilter
{
    private readonly string _lastName;

    public AddLastNameFilter(string lastName)
    {
        _lastName = lastName;
    }

    public async ValueTask<object> InvokeAsync(RouteHandlerFilterContext context, Func<RouteHandlerFilterContext, ValueTask<object>> next)
    {
        var name = (string)context.Parameters[0];
        context.Parameters[0] = $"{name} {_lastName}";
        return await next(context);
    }
}

Metadata

Metadata

Assignees

Labels

Priority:1Work that is critical for the release, but we could probably ship withoutapi-approvedAPI was approved in API review, it can be implementedenhancementThis issue represents an ask for new feature or an enhancement to an existing oneold-area-web-frameworks-do-not-use*DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions