Skip to content

Add support for endpoint filters #37853

Closed
@DamianEdwards

Description

@DamianEdwards

Filters in MVC allow for code to be executed immediately before and after the invocation of action methods, allowing for inspection and modification of input parameters before execution, or even completely short-circuiting the action invocation altogether, along with inspection and modification of action method results, or even completely replacing the result, before they are executed.

There is no equivalent of this today when composing request handlers using raw endpoints however, e.g. via app.MapGet, etc. This issue is proposing adding support for filters as a new intrinsic in the request pipeline. We could consider having MVC support running filters registered via this new mechanism in addition to its own filters feature.

There are two types of filter scenarios to consider:

  1. A "generic" filter that does not target an endpoint or route handler delegate of a particular type. This scenario would require access to the target parameters, etc. after they've been populated by whatever the default logic is before the endpoint/handler is executed, and access to the returned value or exception after the endpoint/hander is executed.
  2. A "specific" filter that explicitly targets an endpoint or route handler of a specific delegate type, e.g. Func<int, string, IResult>. This scenario would require access to the

Both scenarios involve each filter invocation getting passed the delegate representing the next filter in the pipeline, such that they can decide whether or not to invoke the next filter or not.

Generic filter example

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

app.AddEndpointFilter(async (ctx, next) =>
{
    // Log out the parameters
    var logger = ctx.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>()
        .CreateLogger("DemoFilter");

    foreach (var p in ctx.Parameters)
    {
        logger.LogDebug("Parameter name {ParameterName}", p.Name);
    }

    await next();

    // Log the return type
    logger.LogDebug("Instance of {ResultType} returned", ctx.Result?.GetType());
});

app.MapGet("/", () => "Hello World!");
app.MapGet("/{name}", (string name) => $"Hello {name}!");

app.Run();

WIP, more to come

Scratch

//  Defined on IEndpointRouteBuilder?
var filters = app.CreateFilterBuilder()
                 .AddFilter(async (FilterContext ctx, Func<Task<IResult>> next) =>
                 {
                    // Dynamic filter
                    foreach (RouteHandlerParameter p in ctx.Parameters)
                    {
                        if (!MiniValidation.TryValidate(p.Value, out var errors))
                        {
                            return Results.Problem(errors);
                        }
                    }
                    var result = await next();
                    // Do horribly slow thing with result

                    return result;
                 })
                 .Add((Func<IResult> next) =>
                 {
                    var result = next();
                    if (result is OkResult<string> ok)
                    {
                        return ok.Value + ": Hello from a filter!";
                    }
                    return result;
                 })
                 .Add((AppDb db, Func<string> next) =>
                 // This delegate accepts the same params as the its target
                 // *PLUS* the handler itself
                 {
                    var result = next();
                    return Results.Ok(result + ": Hello from a filter!");
                 });

var validationFilters = app.CreateFilterBuilder()
    .Add((AppDb db, HttpContext context, string (AppDb) next) => next() + ": Hello from a filter!")
    // This is an extension method added by a library that adds filters to this pipeline, in this case filters that 
    // perform validation. This filter pipeline can then be passed in to a RouteGroup or directly into any Map* call
    .Validate();

app.MapGet("/root", filters, () => "Hello from root");

// RouteGroup with no name or path, just used for grouping endpoints & applying middleware/filters/metadata to them
app.RouteGroup()
   .UseResponseCompression()
   .AddFilter((HttpContext context, Func<string> next) => next() + ": Filtered!")
   .MapGet("/root", () => "Hello from root");

// RouteGroup with a filter pipeline passed in
var rootGroup1 = app.RouteGroup(filters);
group.UseWhatever();
group.MapGet("/hello", () => "Hello");
group.MapGet("/bye", () => "Bye");

// RouteGroup with a filter pipeline and metadata that will apply to all endpoints
var rootGroup2 = app.RouteGroup(filters)
    .WithTags("group2");
group.MapGet("/hello2", () => "Hello");
group.MapGet("/bye2", () => "Bye");

// RouteGroup with a route prefix, group name, and filter pipeline, plus some metadata that's applied to all endpoints in the group
var fooGroup = app.RouteGroup(routePatternPrefix: "/foo", groupName: "FooGroup", filters)
    .WithTags("foo")
    .RequiresAuthentication();
group.MapGet("/hello", () => "Hello");
group.MapGet("/bye", () => "Bye");

// How can we weave in route matcher policy semantics to enable things like specifying
// a middleware that only runs for a specific Razor page or controller

Metadata

Metadata

Assignees

Labels

Priority:1Work that is critical for the release, but we could probably ship withoutarea-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcenhancementThis issue represents an ask for new feature or an enhancement to an existing onefeature-minimal-actionsController-like actions for endpoint routingold-area-web-frameworks-do-not-use*DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labelstriage-focusAdd this label to flag the issue for focus at triage

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions