← Back to Article List         
Minimal APIs and Endpoint Filters

Minimal APIs and Endpoint Filters

Published on 26 Sep 2026     8 min read Web API
Web API

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:

  • MapGet handles an HTTP GET request.
  • /hello is 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 Request if 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 ProductRequest passed 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, and MapPatch.
  • 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 IEndpointFilter for 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 through AddValidation(). Microsoft Learn