Introduction
ASP.NET Core Minimal APIs have become increasingly popular because they allow developers to build APIs with less code and better performance. However, as applications grow, developers often need to perform common tasks before or after an endpoint executes. Examples include request validation, logging, authorization checks, performance monitoring, and modifying responses.
Before ASP.NET Core Endpoint Filters were introduced, developers typically relied on middleware, action filters, or repetitive code inside endpoints. Endpoint Filters provide a cleaner and more focused way to handle cross-cutting concerns specifically for Minimal APIs.
In this article, you'll learn what Endpoint Filters are, how they work, when to use them, and how to implement them with practical examples.
What Are Endpoint Filters?
Endpoint Filters are components that run before and after a Minimal API endpoint executes.
They act like a pipeline around an endpoint, allowing developers to:
Validate incoming requests
Log request and response information
Modify request data
Modify response data
Handle exceptions
Measure execution time
Apply custom business rules
Think of Endpoint Filters as a security guard standing at the entrance of a building.
Before someone enters:
The guard checks their identity.
The guard verifies permissions.
The guard records visitor information.
After the visitor leaves:
The guard logs departure details.
The guard records the outcome.
Endpoint Filters work similarly around your API endpoints.
Why Were Endpoint Filters Introduced?
Minimal APIs are intentionally lightweight. However, developers quickly discovered that repeating validation and logging code inside every endpoint made applications harder to maintain.
Consider this example:
app.MapPost("/products", (Product product) =>
{
if (string.IsNullOrEmpty(product.Name))
{
return Results.BadRequest("Product name is required.");
}
Console.WriteLine("Product created");
return Results.Ok(product);
});
Now imagine hundreds of endpoints requiring similar validation and logging.
This creates:
Duplicate code
Maintenance challenges
Inconsistent implementations
Endpoint Filters solve this problem by centralizing such logic.
How Endpoint Filters Work
The execution flow looks like this:
Request
↓
Endpoint Filter
↓
Endpoint Logic
↓
Endpoint Filter
↓
Response
The filter can execute code:
Before the endpoint runs
After the endpoint runs
This gives developers full control over the request and response lifecycle.
Creating Your First Endpoint Filter
Let's build a simple logging filter.
Create a new class:
using Microsoft.AspNetCore.Http;
public class LoggingFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
Console.WriteLine($"Request started at {DateTime.UtcNow}");
var result = await next(context);
Console.WriteLine($"Request completed at {DateTime.UtcNow}");
return result;
}
}
Registering the Filter
Attach the filter to a Minimal API endpoint:
app.MapGet("/hello", () =>
{
return "Hello World";
})
.AddEndpointFilter<LoggingFilter>();
When the endpoint is called:
LoggingFilter executes first.
Endpoint logic executes.
LoggingFilter executes again after completion.
Understanding the Key Components
EndpointFilterInvocationContext
This object contains information about the current request.
Example:
var httpContext = context.HttpContext;
You can access:
Request headers
Query parameters
Route values
User information
Services
EndpointFilterDelegate
The next parameter represents the next step in the pipeline.
var result = await next(context);
Calling next() allows execution to continue.
If next() is not called, endpoint execution stops.
Request Validation Example
One of the most common uses of Endpoint Filters is validation.
Suppose we have a Product model:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
Create a validation filter:
public class ProductValidationFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var product = context.GetArgument<Product>(0);
if (string.IsNullOrWhiteSpace(product.Name))
{
return Results.BadRequest("Product name is required.");
}
if (product.Price <= 0)
{
return Results.BadRequest("Price must be greater than zero.");
}
return await next(context);
}
}
Apply the filter:
app.MapPost("/products", (Product product) =>
{
return Results.Ok(product);
})
.AddEndpointFilter<ProductValidationFilter>();
Now validation logic remains separate from business logic.
Measuring API Performance
Performance monitoring is another useful scenario.
using System.Diagnostics;
public class PerformanceFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var stopwatch = Stopwatch.StartNew();
var result = await next(context);
stopwatch.Stop();
Console.WriteLine(
$"Execution Time: {stopwatch.ElapsedMilliseconds} ms");
return result;
}
}
This helps identify slow endpoints in production environments.
Modifying Responses
Endpoint Filters can also modify responses.
public class ResponseWrapperFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var result = await next(context);
return Results.Ok(new
{
Success = true,
Data = result
});
}
}
This creates a consistent API response structure.
Example output:
{
"success": true,
"data": {
"id": 1,
"name": "Laptop"
}
}
Authorization Example
You can implement custom authorization checks.
public class AdminOnlyFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var user = context.HttpContext.User;
if (!user.IsInRole("Admin"))
{
return Results.Unauthorized();
}
return await next(context);
}
}
Join the conversation! Your thoughts help the community grow.