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:
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:
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);
}
}
Apply it:
app.MapDelete("/products/{id}", (int id) =>
{
return Results.Ok();
})
.AddEndpointFilter<AdminOnlyFilter>();
Only administrators can access the endpoint.
Handling Exceptions
Centralized exception handling improves application reliability.
public class ExceptionFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
try
{
return await next(context);
}
catch (Exception ex)
{
return Results.Problem(
title: "Application Error",
detail: ex.Message);
}
}
}
This prevents unhandled exceptions from exposing internal application details.
Applying Multiple Endpoint Filters
You can chain multiple filters together.
app.MapPost("/products", (Product product) =>
{
return Results.Ok(product);
})
.AddEndpointFilter<LoggingFilter>()
.AddEndpointFilter<ProductValidationFilter>()
.AddEndpointFilter<PerformanceFilter>();
Execution order:
Logging Filter
↓
Validation Filter
↓
Performance Filter
↓
Endpoint
↓
Performance Filter
↓
Validation Filter
↓
Logging Filter
This behavior is similar to middleware pipelines.
Endpoint Filters vs Middleware
Many developers wonder when to use Middleware and when to use Endpoint Filters.
| Feature | Middleware | Endpoint Filter |
|---|
| Scope | Entire Application | Specific Endpoint |
| Minimal API Support | Yes | Yes |
| Access Endpoint Parameters | No | Yes |
| Request Validation | Limited | Excellent |
| Response Modification | Yes | Yes |
| Logging | Excellent | Excellent |
Use Middleware when:
Use Endpoint Filters when:
Logic applies to specific endpoints.
Request validation is required.
Endpoint-specific business rules are needed.
Endpoint Filters vs MVC Action Filters
| Feature | MVC Action Filters | Endpoint Filters |
|---|
| MVC Controllers | Supported | Not Applicable |
| Minimal APIs | Not Supported | Supported |
| Lightweight | Moderate | High |
| Performance | Good | Excellent |
If you are using Minimal APIs, Endpoint Filters are usually the preferred solution.
Real-World Use Cases
Organizations commonly use Endpoint Filters for:
For example, in an e-commerce application:
Validation Filter checks product data.
Logging Filter records transactions.
Performance Filter measures execution time.
Authorization Filter verifies user permissions.
Each responsibility remains separate and maintainable.
Advantages of Endpoint Filters
Endpoint Filters provide several benefits:
Limitations of Endpoint Filters
While powerful, Endpoint Filters have some limitations:
Only work with Minimal APIs
Not intended to replace middleware entirely
Complex application-wide concerns still belong in middleware
Can become difficult to manage if too many filters are chained
Understanding when to use them is important.
Best Practices
When working with Endpoint Filters:
Keep filters focused on a single responsibility.
Avoid placing business logic inside filters.
Use filters for validation, logging, and cross-cutting concerns.
Reuse filters across endpoints whenever possible.
Keep execution lightweight.
Combine filters thoughtfully to avoid complexity.
Conclusion
ASP.NET Core Endpoint Filters are a powerful feature designed specifically for Minimal APIs. They provide a clean and reusable way to implement validation, logging, authorization, performance monitoring, exception handling, and response transformation without cluttering endpoint code.
By separating cross-cutting concerns from business logic, Endpoint Filters make applications easier to maintain, test, and scale. Whether you're building a small API or a large enterprise application, understanding Endpoint Filters can significantly improve the structure and quality of your ASP.NET Core projects.
As Minimal APIs continue to gain popularity, Endpoint Filters have become an essential tool that every ASP.NET Core developer should understand and use effectively.