Description
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:
- 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.
- 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