FREE .NET Web API Course! Join Now 🚀

17 min read

Filters in ASP.NET Core - Everything you Need to Know!

#dotnet .NET Web API Zero to Hero Course

In ASP.NET Core Web API, Filters are built-in hooks that let you run custom logic during specific phases of the HTTP request pipeline. They’re designed to keep your controller actions clean by handling cross-cutting concerns like logging, authorization, validation, exception handling, and response shaping.

Web API supports five main filter types within the MVC pipeline: Authorization, Resource, Action, Exception, and Result filters. Each one runs at a specific point in the request lifecycle and can be registered globally, at the controller level, or per-action using attributes.

Starting with .NET 7, Endpoint Filters were introduced for Minimal APIs. These filters allow you to intercept and modify request/response processing at the endpoint level—without the overhead of the full MVC pipeline. They’re ideal for scenarios where you want to apply logic like validation, logging, or transformation to minimal API routes in a clean, composable way.

As of .NET 9, these six filter types represent the complete and modern approach to request interception in ASP.NET Core Web API—giving you the flexibility to build secure, maintainable APIs with minimal duplication.

What Are Filters in ASP.NET Core?

Filters in ASP.NET Core are components that let you execute custom logic at specific points during the processing of HTTP requests in your Web API. They help you cleanly separate concerns like logging, authentication, error handling, or input validation from your controller logic.

In the traditional MVC-based Web API model, filters are categorized by their position in the pipeline—authorization, resource execution, action invocation, result generation, and exception handling. Each filter runs at a defined stage and can either modify the request/response or short-circuit the pipeline altogether.

With the introduction of Minimal APIs, ASP.NET Core added Endpoint Filters—a new lightweight way to intercept and modify requests and responses at the route-handler level, giving you similar benefits in APIs that don’t use controllers.

Filters help make your Web API code more modular, consistent, and maintainable—by moving repeated logic out of action methods and into reusable components.

Why and When to Use Filters

Filters are essential when you need to apply cross-cutting logic across multiple endpoints without duplicating code. They give you fine-grained control over different stages of the request pipeline and help enforce consistency across your Web API.

Why use filters:

  • Separation of concerns: Keep controllers and endpoints focused on business logic.
  • Code reuse: Apply the same logic (e.g., logging, validation) across actions.
  • Global policies: Enforce rules like authorization or auditing centrally.
  • Error handling: Catch and format exceptions in one place.
  • Request/response shaping: Modify data before it hits the action or leaves the result.

When to use filters:

  • You need to run logic before or after actions.
  • You want to validate input globally before reaching the action.
  • You need structured logging around every request.
  • You want consistent exception formatting across the API.
  • You’re building a Minimal API and want to add lightweight middleware per endpoint using Endpoint Filters.

Use filters when middleware is too generic, and action code is too specific. Filters hit the sweet spot for logic that’s tied to the execution of actions, not the entire HTTP pipeline.

Built-in Filter Types

ASP.NET Core provides several built-in filter types, each designed to run at specific stages in the request processing pipeline.

Understanding the different filter types helps you decide where to apply cross-cutting concerns like logging, authorization, caching, or error handling in a clean and maintainable way.

Here’s a breakdown of the built-in filter types available in ASP.NET Core.

Authorization Filters

Authorization filters are the first line of defense in the ASP.NET Core request pipeline. They run before everything else—including model binding and action filters—and determine whether a user is authorized to access a specific endpoint.

You typically won’t need to write your own, because built-in attributes like [Authorize], [AllowAnonymous], and [Authorize(Roles = "Admin")] cover most use cases. But if you enjoy suffering or have a custom authorization system, you can implement your own by using the IAuthorizationFilter or IAsyncAuthorizationFilter interfaces.

Example: Custom Authorization Filter

public class CustomAuthFilter : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
var isAuthorized = CheckIfUserIsAuthorized(context); // pretend this function exists
if (!isAuthorized)
{
context.Result = new ForbidResult(); // Or new UnauthorizedResult();
}
}
private bool CheckIfUserIsAuthorized(AuthorizationFilterContext context)
{
// Custom logic goes here. Access user claims, headers, etc.
var user = context.HttpContext.User;
return user.Identity != null && user.Identity.IsAuthenticated;
}
}

Applying the Filter

You can register it globally:

services.AddControllers(options =>
{
options.Filters.Add<CustomAuthFilter>();
});

Or slap it on a controller/action like a band-aid:

[ServiceFilter(typeof(CustomAuthFilter))]
public class SecretStuffController : Controller
{
public IActionResult GetTopSecretData()
{
return Ok("You probably shouldn't be seeing this.");
}
}

Use these when you need to apply logic before any part of your controller executes, like checking tokens, API keys, or just vibes.

Resource Filters

Resource filters run before model binding and after the rest of the pipeline completes. They’re useful for things like:

  • Short-circuiting requests before expensive stuff happens (like model binding or hitting the database).
  • Implementing caching at the controller level.
  • Measuring performance of the request pipeline.

Basically, if you want to peek inside the request and maybe throw it away before your controller even gets a chance to say “Hi,” this is where you do it.

Implementing a Custom Resource Filter

public class CustomResourceFilter : IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
// Run BEFORE model binding and action execution
var hasHeader = context.HttpContext.Request.Headers.ContainsKey("X-Cool-Header");
if (!hasHeader)
{
context.Result = new ContentResult
{
StatusCode = 400,
Content = "Missing required header."
};
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
// Run AFTER the rest of the pipeline finishes
// You can log stuff here if you enjoy pain
}
}

Time to talk about Action Filters, the overachievers who feel the need to stand awkwardly before and after your controller actions like they’re monitoring a group project and silently judging your code style.


Action Filters

Action filters are used to run logic before and after the execution of a controller action method. This is the sweet spot for things like:

  • Logging
  • Modifying input/output
  • Injecting extra data
  • Auditing user behavior

They don’t mess with model binding—that’s already done by the time these filters kick in—but they do let you observe or influence what happens right around your controller logic.

Creating a Custom Action Filter

public class CustomActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// This runs BEFORE the action method
Console.WriteLine("Before action execution");
}
public void OnActionExecuted(ActionExecutedContext context)
{
// This runs AFTER the action method
Console.WriteLine("After action execution");
if (context.Exception == null && context.Result is ObjectResult result)
{
result.Value = new { data = result.Value, wrapped = true };
}
}
}

When to Use

Use action filters when you want to:

  • Log requests/responses around specific actions.
  • Modify results in a generic way (like wrapping all responses in a standard format).
  • Audit who did what, when, and why they thought it was a good idea.

Don’t use them for things like caching, authentication, or result formatting unless you’re trying to become your own tech debt.

For Minimal APIs? Yeah, no. These don’t apply—go for EndpointFilter.

Exception Filters

Exception filters are your last line of defense when things blow up inside the MVC pipeline. They’re designed to catch unhandled exceptions thrown by controllers or action filters, and let you decide how to respond—without dumping raw stack traces all over your users.

These filters are useful when you want centralized error handling and you’re working within the MVC pipeline (read: controllers, not Minimal APIs).


When to Use

  • Logging unhandled exceptions in a structured way.
  • Converting exceptions into consistent error responses (like ProblemDetails).
  • Avoiding repetitive try/catch logic in every controller method.

Custom Exception Filter Example

public class CustomExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
var exception = context.Exception;
// Log the exception, send it to a service, etc.
Console.WriteLine($"Unhandled exception: {exception.Message}");
// Return a custom response
context.Result = new ObjectResult(new
{
error = "Something went wrong.",
detail = exception.Message // don't expose this in production
})
{
StatusCode = 500
};
context.ExceptionHandled = true; // prevents further propagation
}
}

Result Filters

Result filters let you hook into the part of the pipeline that handles formatting and returning the result—after the action method has executed, but before the response is finalized. Think of it as a chance to modify or wrap the result right before it goes out the door.

They’re ideal for post-processing the result (e.g., adding headers, wrapping responses, logging output), and for doing cleanup tasks after the response is sent.


When to Use

  • Modifying or replacing the action result
  • Wrapping responses in a standard format
  • Adding headers to responses
  • Logging or tracking output data
  • Executing cleanup code after results are processed

Custom Result Filter Example

public class CustomResultFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
// Runs just before the result is executed
if (context.Result is ObjectResult objectResult)
{
objectResult.Value = new
{
success = true,
data = objectResult.Value
};
}
}
public void OnResultExecuted(ResultExecutedContext context)
{
// Runs after the result is executed (response sent)
Console.WriteLine("Response was sent.");
}
}

Endpoint Filters (for Minimal APIs)

Endpoint filters are the lightweight alternative to MVC filters, designed specifically for Minimal APIs in ASP.NET Core. They allow you to inject logic before and after the execution of an endpoint handler—similar to middleware, but scoped to individual routes.

You can use endpoint filters to handle cross-cutting concerns like:

  • Authorization
  • Validation
  • Logging
  • Response shaping
  • Error handling

They offer more control than middleware and are easier to compose on a per-endpoint basis.


Basic Endpoint Filter Example

public class LoggingEndpointFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var endpointName = context.HttpContext.GetEndpoint()?.DisplayName;
Console.WriteLine($"[Before] Executing: {endpointName}");
var result = await next(context);
Console.WriteLine($"[After] Executed: {endpointName}");
return result;
}
}

Applying the Filter to an Endpoint

var app = WebApplication.CreateBuilder(args).Build();
app.MapGet("/greet", (string name) => $"Hello, {name}!")
.AddEndpointFilter<LoggingEndpointFilter>();
app.Run();

Inline Filter Example

You can also write filters inline without creating a separate class:

app.MapPost("/validate", (User user) => Results.Ok(user))
.AddEndpointFilter(async (context, next) =>
{
var user = context.GetArgument<User>(0);
if (string.IsNullOrWhiteSpace(user.Name))
return Results.BadRequest("Name is required.");
return await next(context);
});

  • Filters are executed in the order they’re added.
  • You can add multiple filters to a single endpoint.
  • Filters can short-circuit execution by returning a result early.
  • Ideal for small, focused logic blocks without needing full MVC infrastructure.

Endpoint filters bring structured extensibility to Minimal APIs—without forcing you into full controller-based architecture. Clean, fast, and surprisingly flexible.

Filter Execution Order

Understanding the execution order of filters in ASP.NET Core is important if you don’t want your logic to turn into a tangled mess of “Why is this running now?” and “Why did nothing run at all?”

Each filter type runs at a specific point in the request-processing pipeline. Some wrap everything (like resource filters), some run before and after controller actions (like action and result filters), and others only care when something goes wrong (like exception filters). Here’s how the pipeline flows, assuming you’re using controllers:


Request Pipeline (Execution Phase)

  1. Authorization Filters
    Run first. If authorization fails, the request is short-circuited.
  2. Resource Filters
    Wrap the entire execution pipeline. Can short-circuit before model binding.
  3. Action Filters
    Run before and after the action method.
  4. Exception Filters
    Run only if an unhandled exception is thrown from the action or result execution.
  5. Result Filters
    Run before and after the action result is executed (e.g., formatting a response).

Response Pipeline (Reverse Order for “After” Hooks)

Filters that have “after” logic execute in reverse order from how they were added. This means the last one in is the first one out—like a slightly dysfunctional stack.


Visual Summary (because you probably scrolled straight to this)

--> Authorization Filters
--> Resource Filters
--> Action Filters
--> Controller Action
<-- Action Filters
<-- Result Filters
<-- Resource Filters
Exception Filters (if needed)

Minimal APIs Note

Minimal APIs don’t use this full filter stack. They use Endpoint Filters, which wrap the delegate that runs the endpoint. If you’re using Minimal APIs, you don’t get action, result, or exception filters—just endpoint-level logic.


Important Notes About Filter Execution

  • Order matters.
  • Don’t mix business logic into filters.
  • Keep filters single-purpose and consistent.
  • Yes, you’ll forget the order at least once and debug it for an hour. That’s part of the experience.

Ah yes, the eternal struggle between [TypeFilter] and [ServiceFilter]—two ways of injecting dependencies into your filters, each with slightly different behavior and equal capacity to confuse you when things mysteriously stop working.

Let’s break it down without the fluff.


TypeFilter vs ServiceFilter in ASP.NET Core

Both attributes are used to inject services into filters (because you actually want to use DI like a responsible dev), but they do it differently under the hood.


ServiceFilter

  • Pulls the filter instance from the DI container.
  • Assumes you’ve registered the filter type as a service (e.g., services.AddScoped<YourFilter>()).
  • If the filter has dependencies, they must also be registered in DI.

Example:

[ServiceFilter(typeof(MyCustomFilter))]
public IActionResult DoSomething() => Ok();

Setup:

services.AddScoped<MyCustomFilter>();

Use Case:

  • Use when the filter is already registered with DI and you want to reuse it across endpoints.
  • Throws an error if the filter type isn’t registered.

TypeFilter

  • Creates an instance of the filter type at runtime using ActivatorUtilities.
  • You don’t have to register the filter in DI.
  • Dependencies are resolved using constructor injection on the fly.

Example:

[TypeFilter(typeof(MyCustomFilter))]
public IActionResult DoSomething() => Ok();

Use Case:

  • Use when the filter is not registered in DI, or you want to pass arguments (e.g., constructor parameters).
  • Also good for filters that need runtime parameters using Arguments = new object[] { ... }.
  • Slightly slower due to runtime construction.

  • Use ServiceFilter if the filter is already in DI and you want to reuse it.
  • Use TypeFilter if you’re too lazy to register it or you want to pass constructor arguments manually.

Using Filter Attributes vs Global Filters

In ASP.NET Core, filters can be applied either as attributes on controllers and actions or configured globally in the application setup. Both options serve the same purpose—executing cross-cutting logic at defined points in the request pipeline—but they differ in scope, visibility, and control.

Let’s break down when to use each and what tradeoffs come with them.


Filter Attributes (Scoped per Controller or Action)

Use filter attributes when you want the filter logic to apply only to specific endpoints. This gives you fine-grained control and makes the filter’s presence explicit.

Example:

[ServiceFilter(typeof(LoggingActionFilter))]
public class ProductsController : ControllerBase
{
[HttpGet("/products")]
public IActionResult Get() => Ok("Product list");
}

You can also use [TypeFilter] if you’re not registering the filter in DI, or if you need to pass constructor arguments.


Pros:

  • Explicit—easy to see which actions have filters applied
  • Targeted—applies only where needed
  • Supports per-endpoint behavior

Cons:

  • Repetitive for common/global logic
  • Easy to miss or forget on new actions

Global Filters (Applied to All Controllers/Actions)

Global filters are configured during app startup and apply to every controller and action unless explicitly overridden. Ideal for concerns that should run everywhere (e.g., logging, exception handling, response shaping).

Example (Program.cs / Startup.cs):

services.AddControllers(options =>
{
options.Filters.Add<LoggingActionFilter>();
});

Pros:

  • DRY—define once, use everywhere
  • Great for cross-cutting concerns
  • Cleaner controller code

Cons:

  • Less obvious—may affect endpoints silently
  • Harder to opt out on a per-action basis

Recommendation

Use attributes when:

  • Logic applies only to specific endpoints
  • You want maximum clarity and control

Use global filters when:

  • Logic is truly global (e.g., logging, error handling)
  • You want consistent behavior across all controllers

Mixing both is fine—just keep it organized and document the intent, or you’ll eventually debug a problem that turns out to be a forgotten global filter doing something “helpful.”

Real-World Use Cases

Filters are most useful when you’re dealing with cross-cutting concerns—logic that applies across multiple endpoints but shouldn’t clutter your controller actions. Here’s how filters are typically used in real-world ASP.NET Core applications, without unnecessary magic or overengineering (well, ideally).


Logging

Use: Action Filters or Endpoint Filters

Add structured logging around controller actions or endpoint executions to capture things like method names, parameters, duration, and outcomes.

Example:

public class LoggingFilter : IActionFilter
{
private readonly ILogger<LoggingFilter> _logger;
public LoggingFilter(ILogger<LoggingFilter> logger)
{
_logger = logger;
}
public void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogInformation($"Executing {context.ActionDescriptor.DisplayName}");
}
public void OnActionExecuted(ActionExecutedContext context)
{
_logger.LogInformation($"Executed {context.ActionDescriptor.DisplayName}");
}
}

Caching

Use: Resource Filters

Resource filters can intercept requests early and serve cached responses without running the controller logic.

Example Use Case:

  • Cache output for anonymous GET requests to reduce DB hits.
  • Short-circuit expensive operations if data is already cached.

Validation

Use: Action Filters or Endpoint Filters (for Minimal APIs)

Custom validation filters can inspect the model state or manually validate incoming data before the action is executed.

Example:

public class ValidateModelFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
public void OnActionExecuted(ActionExecutedContext context) { }
}

In Minimal APIs, you’d use an EndpointFilter to do the same thing by inspecting parameters directly.


Auditing

Use: Action Filters

Capture who performed what action, when, and with what data. You can store this in a database or send it to an audit service.

Example Use Case:

  • Log user activity for sensitive operations (e.g., changing account settings).
  • Store before-and-after snapshots of data.

Input Sanitization

Use: Action Filters

Manipulate or sanitize input models before they reach the controller. Useful for trimming strings, removing unwanted HTML, or normalizing values.

Example Use Case:

  • Strip HTML tags from comment inputs.
  • Convert emails to lowercase before validation.

Metrics

Use: Resource Filters or Action Filters

Track execution time, request counts, or error rates for endpoints.

Example:

public class TimingFilter : IActionFilter
{
private readonly Stopwatch _stopwatch = new();
public void OnActionExecuting(ActionExecutingContext context)
{
_stopwatch.Start();
}
public void OnActionExecuted(ActionExecutedContext context)
{
_stopwatch.Stop();
var elapsed = _stopwatch.ElapsedMilliseconds;
Console.WriteLine($"Action took {elapsed}ms");
}
}

In production, you’d send this to something like Prometheus, Application Insights, or OpenTelemetry.


Filters vs Middleware: When to Choose What

Filters and middleware both let you inject logic into the request-processing pipeline, but they operate at different levels and serve different purposes. Mixing them up is a quick way to turn your app into a spaghetti factory with no exit plan.

Here’s how to know which tool to reach for—without rewriting your entire architecture three times.


Middleware

Middleware runs outside the MVC pipeline and wraps the entire HTTP request. It’s the first and last thing that touches a request and response. Middleware has no concept of controllers, actions, routing, or model binding—it just deals with raw HttpContext.

Read more about Middlewares in ASP.NET Core: codewithmukesh.com/blog/middlewares-in-aspnet-core/

Use Middleware When:

  • You need logic that applies to all requests, regardless of endpoint type (MVC, Razor Pages, Minimal APIs, static files, etc.)
  • You don’t care about the controller/action layer
  • You need early control of the request (e.g., authentication, logging, CORS, request buffering)
  • You want to manipulate response headers globally

Examples:

  • Authentication/authorization middleware (UseAuthentication, UseAuthorization)
  • Logging and telemetry
  • Response compression
  • Custom headers for all responses

Filters

Filters run inside the MVC or Minimal API pipeline. They’re aware of routing, model binding, action results, and exceptions. They’re designed for controller-specific or endpoint-specific logic.

Use Filters When:

  • You’re working inside MVC or Minimal APIs
  • You need access to action parameters, model state, or action results
  • You want to inject logic around specific controllers/actions
  • You need per-endpoint behaviors like validation, logging, wrapping results, etc.

Examples:

  • Validating model state
  • Wrapping responses in a consistent shape
  • Custom authorization logic per controller
  • Auditing which user performed which action

Takeaways

  • Use middleware for broad, low-level concerns that apply to all requests.
  • Use filters for high-level, per-endpoint logic inside MVC or Minimal APIs.
  • If you need access to route data, controller actions, or model binding: filters.
  • If you need access before routing, or outside MVC entirely: middleware.

Summary

Filters in ASP.NET Core are used to inject logic into specific parts of the request pipeline, especially inside the MVC or Minimal API frameworks. They’re ideal for handling concerns like logging, validation, error responses, and more, without cluttering your controller or endpoint code.

In MVC, you have several types of filters—authorization, resource, action, result, and exception—each tied to a different phase of the request lifecycle. Minimal APIs use endpoint filters instead, which provide similar control at the route level.

Filters can be applied globally or directly on specific actions or controllers using attributes. If your filter needs dependencies, you can inject them using dependency injection with either ServiceFilter, TypeFilter, or by registering the filter as a service.

Middleware is still the go-to choice for application-wide concerns that happen before routing, but when your logic depends on the action context or model state, filters are the right tool. Use them when you need endpoint-level precision without rewriting the same logic everywhere.

If you found this helpful (or at least slightly less painful than reading the official docs), go ahead and like and share it with your fellow devs.


Also, if you want to dive deeper into building clean, production-ready APIs, check out our .NET Web API course. It covers everything from routing and model binding to advanced topics like versioning, testing, and of course, using filters the right way.

Join 8,000+ .NET Students and Developers

Support ❤️
If you have enjoyed my content, support me by buying a couple of coffees.
Share this Article
Share this article with your network to help others!
What's your Feedback?
Do let me know your thoughts around this article.

Level Up Your .NET Skills

Join my community of 8,000+ developers and architects.
Each week you will get 1 practical tip with best practices and real-world examples.