Minimal APIs gained popularity right away when they were released in.NET 6 because they made web services speedy and light. Nevertheless, previous iterations lacked a suitable method for managing routine tasks like as permission, logging, and validation without having to repeat the same code. This is where Endpoint Filters come in; they were first introduced in.NET 7, got better in.NET 8, and are now much better in.NET 9, offering more control over the request pipeline, grouping options, and ease of use.
This article will examine filters in Minimal APIs with.NET 9 and show you how to utilize them efficiently while adhering to current best practices.
What is New in .NET 9 for Filters?
The core API remains the same, but .NET 9 brings several improvements that make filters easier and more powerful to use:
- Better dependency injection (DI) and setup for IEndpointFilterFactory
- Support for filter composition and grouping
- Smarter type inference for inline filters
- More consistent behavior between MapGroup and individual endpoints
If you have used filters in .NET 7 or 8, they’ll still work — but in .NET 9, you’ll enjoy cleaner code, stronger typing, and simpler configuration.
What Are Filters?
Filters let you add custom logic into the request pipeline of a Minimal API endpoint — both before and after the handler runs.
You can think of them as lightweight middleware but applied only to specific endpoints or endpoint groups.
They’re great for handling common tasks like:
- Validation
- Authentication / Authorization
- Logging / Metrics
- Exception Handling
- Response Wrapping
- The source code can be downloaded from GitHub.
The tools that I have used here
1. .NET 9.0
2. Visual Studio 2026 Insider
Defining Filters in .NET 9
You can create filters in two main ways:
- Inline Filters: Write quick, simple logic directly next to the endpoint.
- Reusable Filters: Create separate classes that implement IEndpointFilter for cleaner, reusable code.
Let us look at both.
Inline Filters
The simplest way to add a filter is inline using AddEndpointFilter
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapGet("/inlinefilters", (string name) => $"Hello, {name}!")
.AddEndpointFilter(async (context, next) =>
{
Console.WriteLine("Before endpoint logic");
var result = await next(context);
Console.WriteLine("After endpoint logic");
return result;
});
app.Run();
How It Works
- You receive a context containing arguments, HttpContext, and more.
- You can run code before or after calling next(context).
- You can modify the result or even stop the request early.
Reusable Filters (IEndpointFilter)
When logic needs to be reused or tested, define a class-based filter:
public class LoggingFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
Console.WriteLine($"Incoming: {context.HttpContext.Request.Path}");
var result = await next(context);
Console.WriteLine($"Completed: {context.HttpContext.Response.StatusCode}");
return result;
}
}
Attach to the endpoint as below:
// Using a reusable filter
app.MapGet("/reusefilter", (string name) => $"Hello, {name}!")
.AddEndpointFilter<WebApplication1.Filters.LoggingFilter>();
Practical Use Cases
Here are some real-world examples of how filters can be used in .NET 9 projects:
Validation Filter
Perform model validation before hitting your endpoint logic.
public class ValidationFilter<UserDto> : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var model = context.Arguments.OfType<UserDto>().FirstOrDefault();
if (model == null)
{
return Results.BadRequest("Invalid request payload.");
}
// Use reflection to check for UserName, Name, and Age properties
var type = typeof(UserDto);
var nameProp = type.GetProperty("Name");
var ageProp = type.GetProperty("Age");
var name = nameProp?.GetValue(model) as string;
var ageValue = ageProp?.GetValue(model);
int age = 0;
if (ageValue != null && int.TryParse(ageValue.ToString(), out var parsedAge))
{
age = parsedAge;
}
if (string.IsNullOrWhiteSpace(name) || age <= 0)
{
return Results.BadRequest("Invalid request payload. Name and Age are required and Age must be greater than 0.");
}
var validationResults = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(
model, new ValidationContext(model), validationResults, true);
if (!isValid)
{
var errors = validationResults.ToDictionary(
r => r.MemberNames.FirstOrDefault() ?? "Unknown",
r => new[] { r.ErrorMessage ?? "Validation error" });
return Results.ValidationProblem(errors);
}
// Await the next delegate in the pipeline
return await next(context);
}
}
Attach to endpoint as below
// Using a validation filter
app.MapPost("/validationfilter", (WebApplication1.Models.UserDto user) => Results.Ok($"User {user.Name} of age {user.Age} created."))
.AddEndpointFilter<ValidationFilter<WebApplication1.Models.UserDto>>();
Authorization Filter
Implement role-based access without repeating logic.
public class RoleFilter : IEndpointFilter
{
private readonly string _role;
public RoleFilter(string role) => _role = role;
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var user = context.HttpContext.User;
if (!user.Identity?.IsAuthenticated ?? false)
return Results.Unauthorized();
if (!user.IsInRole(_role))
return Results.Forbid();
return await next(context);
}
}
Attach to the endpoint:
//using role based filter
app.MapGet("/rolefilter", () => "Welcome, Admin")
.AddEndpointFilterFactory((factoryContext, next) =>
(context) =>
{
var filter = new RoleFilter("Admin");
return filter.InvokeAsync(context, next);
});
Exception Handler Filter
Centralize error handling at the endpoint or group level.
public class ExceptionFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
try
{
return await next(context);
}
catch (Exception ex)
{
Console.WriteLine($"Exception: {ex.Message}");
return Results.Problem("Something went wrong, please try again.");
}
}
}
Apply at a group level in .NET 9:
public class ExceptionFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
try
{
return await next(context);
}
catch (Exception ex)
{
Console.WriteLine($"Exception: {ex.Message}");
return Results.Problem("Something went wrong, please try again.");
}
}
}
Attach to the endpoint:
// Using a global exception handling filter
var api = app.MapGroup("/api")
.AddEndpointFilter<ExceptionFilter>();
api.MapGet("/exceptionfilter", () => { throw new Exception("Test exception"); });
Conclusion
.NET 9 filters Common activities like validation, logging, and error handling can be easily handled in one location with minimal APIs. They guarantee that shared logic remains constant throughout your application while keeping your endpoint code clear and concise. For greater control, you can apply filters to particular endpoints or groups without introducing the intricacy of global middleware. Filters aid in maintaining readable, well-structured, and reusable code as your application expands. To put it briefly, they're an excellent method for creating dependable, clear, and stable minimal APIs. Happy Coding!
European Best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.
