Filters in Minimal API apps
Note
This isn't the latest version of this article. For the current release, see the .NET 8 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see .NET and .NET Core Support Policy. For the current release, see the .NET 8 version of this article.
Important
This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
For the current release, see the .NET 8 version of this article.
By Fiyaz Bin Hasan, Martin Costello, and Rick Anderson
Minimal API filters allow developers to implement business logic that supports:
- Running code before and after the endpoint handler.
- Inspecting and modifying parameters provided during an endpoint handler invocation.
- Intercepting the response behavior of an endpoint handler.
Filters can be helpful in the following scenarios:
- Validating the request parameters and body that are sent to an endpoint.
- Logging information about the request and response.
- Validating that a request is targeting a supported API version.
Filters can be registered by providing a Delegate that takes a EndpointFilterInvocationContext
and returns a EndpointFilterDelegate
. The EndpointFilterInvocationContext
provides access to the HttpContext
of the request and an Arguments
list indicating the arguments passed to the handler in the order in which they appear in the declaration of the handler.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string ColorName(string color) => $"Color specified: {color}!";
app.MapGet("/colorSelector/{color}", ColorName)
.AddEndpointFilter(async (invocationContext, next) =>
{
var color = invocationContext.GetArgument<string>(0);
if (color == "Red")
{
return Results.Problem("Red not allowed!");
}
return await next(invocationContext);
});
app.Run();
The preceding code:
- Calls the
AddEndpointFilter
extension method to add a filter to the/colorSelector/{color}
endpoint. - Returns the color specified except for the value
"Red"
. - Returns Results.Problem when the
/colorSelector/Red
is requested. - Uses
next
as theEndpointFilterDelegate
andinvocationContext
as theEndpointFilterInvocationContext
to invoke the next filter in the pipeline or the request delegate if the last filter has been invoked.
The filter is run before the endpoint handler. When multiple AddEndpointFilter
invocations are made on a handler:
- Filter code called before the
EndpointFilterDelegate
(next
) is called are executed in order of First In, First Out (FIFO) order. - Filter code called after the
EndpointFilterDelegate
(next
) is called are executed in order of First In, Last Out (FILO) order.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
app.Logger.LogInformation(" Endpoint");
return "Test of multiple filters";
})
.AddEndpointFilter(async (efiContext, next) =>
{
app.Logger.LogInformation("Before first filter");
var result = await next(efiContext);
app.Logger.LogInformation("After first filter");
return result;
})
.AddEndpointFilter(async (efiContext, next) =>
{
app.Logger.LogInformation(" Before 2nd filter");
var result = await next(efiContext);
app.Logger.LogInformation(" After 2nd filter");
return result;
})
.AddEndpointFilter(async (efiContext, next) =>
{
app.Logger.LogInformation(" Before 3rd filter");
var result = await next(efiContext);
app.Logger.LogInformation(" After 3rd filter");
return result;
});
app.Run();
In the preceding code, the filters and endpoint log the following output:
Before first filter
Before 2nd filter
Before 3rd filter
Endpoint
After 3rd filter
After 2nd filter
After first filter
The following code uses filters that implement the IEndpointFilter
interface:
using Filters.EndpointFilters;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
app.Logger.LogInformation("Endpoint");
return "Test of multiple filters";
})
.AddEndpointFilter<AEndpointFilter>()
.AddEndpointFilter<BEndpointFilter>()
.AddEndpointFilter<CEndpointFilter>();
app.Run();
In the preceding code, the filters and handlers logs show the order they are run:
AEndpointFilter Before next
BEndpointFilter Before next
CEndpointFilter Before next
Endpoint
CEndpointFilter After next
BEndpointFilter After next
AEndpointFilter After next
Filters implementing the IEndpointFilter
interface are shown in the following example:
namespace Filters.EndpointFilters;
public abstract class ABCEndpointFilters : IEndpointFilter
{
protected readonly ILogger Logger;
private readonly string _methodName;
protected ABCEndpointFilters(ILoggerFactory loggerFactory)
{
Logger = loggerFactory.CreateLogger<ABCEndpointFilters>();
_methodName = GetType().Name;
}
public virtual async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
Logger.LogInformation("{MethodName} Before next", _methodName);
var result = await next(context);
Logger.LogInformation("{MethodName} After next", _methodName);
return result;
}
}
class AEndpointFilter : ABCEndpointFilters
{
public AEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}
class BEndpointFilter : ABCEndpointFilters
{
public BEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}
class CEndpointFilter : ABCEndpointFilters
{
public CEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}
Validate an object with a filter
Consider a filter that validates a Todo
object:
app.MapPut("/todoitems/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
}).AddEndpointFilter(async (efiContext, next) =>
{
var tdparam = efiContext.GetArgument<Todo>(0);
var validationError = Utilities.IsValid(tdparam);
if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(efiContext);
});
In the preceding code:
- The
EndpointFilterInvocationContext
object provides access to the parameters associated with a particular request issued to the endpoint via theGetArguments
method. - The filter is registered using a
delegate
that takes aEndpointFilterInvocationContext
and returns aEndpointFilterDelegate
.
In addition to being passed as delegates, filters can be registered by implementing the IEndpointFilter
interface. The following code shows the preceding filter encapsulated in a class which implements IEndpointFilter
:
public class TodoIsValidFilter : IEndpointFilter
{
private ILogger _logger;
public TodoIsValidFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<TodoIsValidFilter>();
}
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext efiContext,
EndpointFilterDelegate next)
{
var todo = efiContext.GetArgument<Todo>(0);
var validationError = Utilities.IsValid(todo!);
if (!string.IsNullOrEmpty(validationError))
{
_logger.LogWarning(validationError);
return Results.Problem(validationError);
}
return await next(efiContext);
}
}
Filters that implement the IEndpointFilter
interface can resolve dependencies from Dependency Injection(DI), as shown in the previous code. Although filters can resolve dependencies from DI, filters themselves can not be resolved from DI.
The ToDoIsValidFilter
is applied to the following endpoints:
app.MapPut("/todoitems2/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
}).AddEndpointFilter<TodoIsValidFilter>();
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
}).AddEndpointFilter<TodoIsValidFilter>();
The following filter validates the Todo
object and modifies the Name
property:
public class TodoIsValidUcFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext efiContext,
EndpointFilterDelegate next)
{
var todo = efiContext.GetArgument<Todo>(0);
todo.Name = todo.Name!.ToUpper();
var validationError = Utilities.IsValid(todo!);
if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(efiContext);
}
}
Register a filter using an endpoint filter factory
In some scenarios, it might be necessary to cache some of the information provided in the MethodInfo
in a filter. For example, let's assume that we wanted to verify that the handler an endpoint filter is attached to has a first parameter that evaluates to a Todo
type.
app.MapPut("/todoitems/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
}).AddEndpointFilterFactory((filterFactoryContext, next) =>
{
var parameters = filterFactoryContext.MethodInfo.GetParameters();
if (parameters.Length >= 1 && parameters[0].ParameterType == typeof(Todo))
{
return async invocationContext =>
{
var todoParam = invocationContext.GetArgument<Todo>(0);
var validationError = Utilities.IsValid(todoParam);
if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(invocationContext);
};
}
return invocationContext => next(invocationContext);
});
In the preceding code:
- The
EndpointFilterFactoryContext
object provides access to theMethodInfo
associated with the endpoint's handler. - The signature of the handler is examined by inspecting
MethodInfo
for the expected type signature. If the expected signature is found, the validation filter is registered onto the endpoint. This factory pattern is useful to register a filter that depends on the signature of the target endpoint handler. - If a matching signature isn't found, then a pass-through filter is registered.
Register a filter on controller actions
In some scenarios, it might be necessary to apply the same filter logic for both route-handler based endpoints and controller actions. For this scenario, it is possible to invoke AddEndpointFilter
on ControllerActionEndpointConventionBuilder
to support executing the same filter logic on actions and endpoints.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapController()
.AddEndpointFilter(async (efiContext, next) =>
{
efiContext.HttpContext.Items["endpointFilterCalled"] = true;
var result = await next(efiContext);
return result;
});
app.Run();
Additional Resources
ASP.NET Core