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:

Think of Endpoint Filters as a security guard standing at the entrance of a building.

Before someone enters:

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:

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:

  1. LoggingFilter executes first.

  2. Endpoint logic executes.

  3. 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:

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.

FeatureMiddlewareEndpoint Filter
ScopeEntire ApplicationSpecific Endpoint
Minimal API SupportYesYes
Access Endpoint ParametersNoYes
Request ValidationLimitedExcellent
Response ModificationYesYes
LoggingExcellentExcellent

Use Middleware when:

Use Endpoint Filters when:

Endpoint Filters vs MVC Action Filters

FeatureMVC Action FiltersEndpoint Filters
MVC ControllersSupportedNot Applicable
Minimal APIsNot SupportedSupported
LightweightModerateHigh
PerformanceGoodExcellent

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:

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:

Understanding when to use them is important.

Best Practices

When working with Endpoint Filters:

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.