Minimal API lets you create HTTP endpoints without controller classes. An Endpoint Filter runs code before and after a Minimal API endpoint handler.
They work together: the endpoint performs the operation, while the filter handles reusable checks such as input validation.
1. What is Minimal API?
A Minimal API is a way to build ASP.NET Core APIs by mapping URLs directly to handler functions.
For example:
app.MapGet("/hello", () => "Hello, Syed!");
Here:
MapGethandles an HTTP GET request./hellois the URL path.() => "Hello, Syed!"is the endpoint handler—the function that processes the request.
Calling:
GET /hello
Returns:
Hello, Syed!
You do not need a controller, [ApiController], or [HttpGet] for this endpoint.
2. Controller API vs Minimal API
Both approaches run on ASP.NET Core and support dependency injection, authentication, authorization, and database access. Minimal APIs offer a more direct way to define handlers. Microsoft Learn
| Feature | Controller API | Minimal API |
|---|---|---|
| Endpoint definition | Controller action | Mapped handler function |
| GET route | [HttpGet] |
MapGet() |
| POST route | [HttpPost] |
MapPost() |
| Common response helpers | Ok(), NotFound() |
Results.Ok(), Results.NotFound() |
| Filters around handler logic | MVC action filters | Endpoint filters |
| Organization | Controllers and actions | Route groups, methods, and separate endpoint files |
| Controller registration | AddControllers() and MapControllers() |
Not required for Minimal API endpoints |
Controller example:
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpGet]
public IActionResult GetProducts()
{
return Ok(new[] { "Laptop", "Mouse" });
}
}
Equivalent Minimal API:
app.MapGet("/api/products", () =>
{
return Results.Ok(new[] { "Laptop", "Mouse" });
});
“Minimal” describes the amount of setup code. It does not mean the API must have limited functionality.
3. Why use Minimal APIs?
- Less setup code for defining endpoints.
- Convenient for focused services and small APIs.
- Supports the same application services and dependency injection you already use.
- Endpoints can be organized into separate files and route groups as the application grows.
You do not have to put all business logic in Program.cs. A handler can call an application service just like a controller action.
4. What is an Endpoint Filter?
An Endpoint Filter wraps the execution of a Minimal API handler.
It can:
- Inspect the handler’s bound arguments.
- Validate input.
- Run logging code.
- Allow execution to continue.
- Return a response immediately and skip the handler.
- Inspect or replace the result returned by the handler.
Filters are registered using AddEndpointFilter(). A reusable filter class implements IEndpointFilter. Microsoft Learn
Simple scenario:
Before processing a product request:
- Check whether its name is empty.
- Check whether its price is greater than zero.
- Return
400 Bad Requestif validation fails. - Otherwise, allow the endpoint to execute.
5. Full simple program
This example works with .NET 8 or later and needs no additional NuGet packages.
Create an empty ASP.NET Core project:
dotnet new web -n MinimalApiDemo
cd MinimalApiDemo
Create the following three files.
File 1: ProductRequest.cs
namespace MinimalApiDemo;
public class ProductRequest
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
This DTO represents the JSON submitted by the client.
File 2: ProductValidationFilter.cs
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace MinimalApiDemo;
public class ProductValidationFilter : IEndpointFilter
{
private readonly ILogger<ProductValidationFilter> _logger;
public ProductValidationFilter(
ILogger<ProductValidationFilter> logger)
{
_logger = logger;
}
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
_logger.LogInformation("Filter: before endpoint");
// ProductRequest is the endpoint's first parameter.
var product = context.GetArgument<ProductRequest>(0);
if (string.IsNullOrWhiteSpace(product.Name))
{
return Results.BadRequest(new
{
message = "Product name is required."
});
}
if (product.Price <= 0)
{
return Results.BadRequest(new
{
message = "Price must be greater than zero."
});
}
// Continue to the next filter or endpoint handler.
var result = await next(context);
_logger.LogInformation("Filter: after endpoint");
return result;
}
}
File 3: Program.cs
using MinimalApiDemo;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseHttpsRedirection();
// This endpoint does not have our validation filter.
app.MapGet("/api/products", () =>
{
var products = new[]
{
new { Id = 1, Name = "Laptop", Price = 55000 },
new { Id = 2, Name = "Mouse", Price = 800 }
};
return Results.Ok(products);
});
// Attach the validation filter only to this endpoint.
app.MapPost("/api/products", (
ProductRequest product,
ILogger<Program> logger) =>
{
logger.LogInformation("Endpoint: processing product");
// Demonstration only: no database write.
return Results.Ok(new
{
message = "Product request processed successfully.",
name = product.Name,
price = product.Price
});
})
.AddEndpointFilter<ProductValidationFilter>();
app.Run();
The GET endpoint returns a fixed list. The POST endpoint demonstrates validation and execution; it does not add a product to that list.
6. Test the program
Run the application and use its displayed address in Postman.
Valid request:
POST https://localhost:7001/api/products
Content-Type: application/json
{
"name": "Laptop",
"price": 55000
}
Replace 7001 with your application’s HTTPS port.
Response: 200 OK
{
"message": "Product request processed successfully.",
"name": "Laptop",
"price": 55000
}
Log messages appear in this order:
Filter: before endpoint
Endpoint: processing product
Filter: after endpoint
Invalid request:
{
"name": "Laptop",
"price": 0
}
Response: 400 Bad Request
{
"message": "Price must be greater than zero."
}
The endpoint does not execute because the filter returns before calling next(context).
7. Explanation of the filter code
A. IEndpointFilter
public class ProductValidationFilter : IEndpointFilter
Identifies the class as an endpoint filter.
The interface requires an InvokeAsync() method.
B. EndpointFilterInvocationContext
EndpointFilterInvocationContext context
Provides access to:
| Property or method | Purpose |
|---|---|
context.HttpContext |
Access request, response, user, and services |
context.Arguments |
Access the endpoint handler’s arguments |
context.GetArgument<T>(index) |
Read a particular argument using its type |
For example:
var product = context.GetArgument<ProductRequest>(0);
The index is 0 because ProductRequest is the first handler parameter:
(ProductRequest product, ILogger<Program> logger)
If you change the parameter order, update the index. This particular filter is intended for handlers whose first parameter is ProductRequest. Microsoft Learn
C. EndpointFilterDelegate next
var result = await next(context);
Calls the next filter, or the endpoint handler if there are no more filters.
- Code before this line runs before the handler.
- Code after this line runs after the handler returns.
- Returning without calling it skips the rest of the filter chain and the handler.
D. ValueTask<object?>
public async ValueTask<object?> InvokeAsync(...)
This is the return type required by IEndpointFilter.
It supports asynchronous execution and can return an endpoint result such as:
Results.Ok(...)
Results.BadRequest(...)
Results.NotFound(...)
E. “After endpoint” does not mean “response already sent”
After await next(context), the handler has returned its result. The result generally has not yet been executed and serialized into the HTTP response.
That is why a filter can still replace it:
var result = await next(context);
// Inspect or replace the result here.
return result;
Also, code after await next(context) is skipped if the handler throws. Use try/finally when cleanup must run regardless of success.
8. Multiple filters: execution order
Suppose you register:
app.MapGet("/demo", () =>
{
Console.WriteLine("Endpoint");
return Results.Ok("Done");
})
.AddEndpointFilter(async (context, next) =>
{
Console.WriteLine("Filter A: Before");
var result = await next(context);
Console.WriteLine("Filter A: After");
return result;
})
.AddEndpointFilter(async (context, next) =>
{
Console.WriteLine("Filter B: Before");
var result = await next(context);
Console.WriteLine("Filter B: After");
return result;
});
The normal execution order is:
Filter A: Before
Filter B: Before
Endpoint
Filter B: After
Filter A: After
Before-handler code follows registration order. After-handler code runs in reverse order. Microsoft Learn
9. Middleware vs Endpoint Filter
| Aspect | Middleware | Endpoint Filter |
|---|---|---|
| Where it operates | HTTP request pipeline | Around a Minimal API handler |
| Main context | HttpContext |
HttpContext plus bound handler arguments |
| Typical scope | All requests reaching it, or a pipeline branch | Selected endpoints or route groups |
| Typical uses | Exception handling, authentication, request logging | Argument validation, handler-specific logging, result changes |
| Can stop execution? | Yes | Yes |
| Direct access to the bound DTO? | Not normally | Yes |
Example:
- Log every incoming HTTP request → middleware.
- Validate the
ProductRequestpassed to a particular handler → endpoint filter. - Authenticate users → ASP.NET Core authentication.
- Require permission on a Minimal API → authorization with
RequireAuthorization().
Use the built-in authentication and authorization system for security rules.
10. Endpoint Filter vs MVC Action Filter
They have a similar purpose—running logic around handler execution—but belong to different programming models.
| MVC Action Filter | Endpoint Filter |
|---|---|
| Used with controller actions | Used here with Minimal API handlers |
Implements IActionFilter or IAsyncActionFilter |
Implements IEndpointFilter |
| Accesses action arguments | Accesses handler arguments |
| Registered through MVC filters or attributes | Registered with AddEndpointFilter() |
MVC action filters do not automatically apply to Minimal API handlers.
11. Key points
- Minimal APIs define endpoints without controller classes.
- Use
MapGet,MapPost,MapPut,MapDelete, andMapPatch. - An endpoint handler can receive DTOs and injected services as parameters.
- Endpoint filters run after argument binding; malformed input can fail before the filter executes.
- Implement
IEndpointFilterfor reusable filter logic. - Use
AddEndpointFilter<T>()to attach a filter. - Call
next(context)to continue execution. - Return directly to skip the handler.
- Keep business logic in services as the application grows.
- This example validates manually. In .NET 8, Minimal APIs do not automatically run Data Annotation validation like
[ApiController]controllers do; .NET 10 adds built-in validation support enabled throughAddValidation(). Microsoft Learn