How Filters Are Implemented and Applied in ASP.NET Core: Architecture and Pipeline

ASP.NET Core implements filters through a pluggable pipeline where IFilterProvider implementations discover filter metadata, FilterFactory caches and materializes instances, and ControllerActionInvoker executes them in ordered stages around actions, while minimal APIs use IEndpointFilter with RequestDelegateFilterPipelineBuilder for lightweight per-endpoint interception.

The filter system in the dotnet/aspnetcore repository provides a cross-cutting concern mechanism that intercepts requests for both MVC controllers and minimal API endpoints. Understanding how filters are implemented and applied in ASP.NET Core requires examining the interplay between filter metadata interfaces, provider abstractions, and the execution pipeline built by the framework.

Core Filter Components

The filter architecture centers on several key contracts and implementations located in the MVC abstractions and core layers.

Filter Metadata Interfaces define the capability contracts that concrete filters implement. Located in src/Mvc/Mvc.Abstractions/src/Filters/*.cs, these include:

  • IFilterMetadata – The base interface that all filters must implement.
  • IActionFilter and IAsyncActionFilter – For running logic before and after action methods.
  • IResultFilter – For intercepting action results.
  • IExceptionFilter – For handling unhandled exceptions.
  • IAuthorizationFilter – For authorization checks.

IFilterProvider defines the contract for discovering filters for a given request. The default implementation at src/Mvc/Mvc.Core/src/Filters/DefaultFilterProvider.cs populates FilterItem.Filter for each FilterDescriptor by resolving instances from attributes, global registration, or custom sources.

FilterFactory at src/Mvc/Mvc.Core/src/Filters/FilterFactory.cs serves as the central hub. It coordinates all providers, orders the filters using FilterDescriptorOrderComparer, caches reusable filters, and creates the final IFilterMetadata[] array used at runtime.

The Filter Implementation Pipeline

The process of how filters are implemented and applied follows a strict lifecycle across five distinct stages.

Discovery

When a request targets a controller action or minimal API endpoint, ASP.NET Core creates an ActionContext (or EndpointBuilder). The framework identifies candidate filters from attributes applied to controllers, actions, or global registration in Startup.cs.

Provider Execution

All registered IFilterProvider implementations (by default only DefaultFilterProvider) run OnProvidersExecuting. For each FilterDescriptor, the provider calls DefaultFilterProvider.ProvideFilter:

  • If the descriptor references a concrete filter instance, it is stored directly in the FilterItem.
  • If it implements IFilterFactory, the factory creates the filter instance, and the provider marks it reusable based on the IsReusable property.

The FilterProviderContext at src/Mvc/Mvc.Abstractions/src/Filters/FilterProviderContext.cs carries the ActionContext and mutable list of FilterItems, allowing providers to add, replace, or remove filters dynamically.

Caching

If every static filter is reusable and only the default provider is present, the resulting filter array is cached on the ActionDescriptor via CachedReusableFilters. This avoids re-instantiating filters on every request, significantly improving performance.

Invocation

The ControllerActionInvoker at src/Mvc/Mvc.Core/src/Infrastructure/ControllerActionInvoker.cs iterates through the resolved filter list, executing them in the following pipeline stages:

  1. AuthorizationIAuthorizationFilter instances run first.
  2. ResourceIResourceFilter logic executes (if present).
  3. ActionIActionFilter or IAsyncActionFilter methods run before and after the action.
  4. ExceptionIExceptionFilter handles unhandled exceptions.
  5. ResultIResultFilter executes around the result processing.
  6. Always-run result – Final result filters guaranteed to execute.

For async filters, each stage awaits the filter’s InvokeAsync method, allowing pre- and post-processing around the inner delegate.

MVC Action Invocation vs. Minimal API Endpoints

ASP.NET Core provides two distinct filter implementations depending on the programming model.

MVC Action Invoker Pipeline

For controller-based MVC applications, the ControllerActionInvoker builds a nested delegate chain. Each filter implements On[Stage]Executing and On[Stage]Executed (or async variants), creating a Russian doll pattern around the actual action method execution. The invoker respects execution order defined by IOrderedFilter and scopes (global → controller → action).

Minimal API Endpoint Filters

For minimal API route handlers, the same concept uses IEndpointFilter defined in src/Http/Http.Abstractions/src/IEndpointFilter.cs. The pipeline is constructed by RequestDelegateFilterPipelineBuilder at src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs.

When you call AddEndpointFilter, the builder stores a list of EndpointFilterFactory delegates. The Create method builds a pipeline by wrapping the original RequestDelegate with these factories, producing a RequestDelegate that runs the filters before invoking the handler.

Ordering, Caching, and Reusability

Three critical design patterns govern filter behavior.

Execution Order – Filters implement IOrderedFilter (default order = 0). The FilterDescriptorOrderComparer sorts them so that global filters run before controller-level, which run before action-level filters. Lower order values execute first.

Filter Reusability – Stateless filters can mark IsReusable = true. Reusable filters are cached and shared across requests, reducing allocation pressure. The FilterFactory checks this flag when building the final filter array.

Filter Containers – Filters implementing IFilterContainer receive a reference to their original descriptor (FilterDefinition), enabling later introspection of filter metadata during execution.

Practical Implementation Examples

The following examples demonstrate how to implement filters in both MVC and minimal API contexts.

Async Action Filter for MVC

public class LogActionFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context,
                                              ActionExecutionDelegate next)
    {
        Console.WriteLine($"Executing {context.ActionDescriptor.DisplayName}");
        var resultContext = await next();   // invoke the action
        Console.WriteLine($"Executed {context.ActionDescriptor.DisplayName}");
    }
}

Register globally so DefaultFilterProvider resolves it via DI:

services.AddControllers(options =>
{
    options.Filters.Add<LogActionFilter>();   // resolved via DI at request time
});

Endpoint Filter for Minimal APIs

app.MapGet("/hello", async (HttpContext ctx) =>
{
    await ctx.Response.WriteAsync("Hello");
})
.AddEndpointFilter(async (context, next) =>
{
    Console.WriteLine("Before handler");
    var result = await next(context);
    Console.WriteLine("After handler");
    return result;
});

Behind the scenes, RequestDelegateFilterPipelineBuilder.Create composes this factory into the final request delegate.

Summary

  • Filter metadata interfaces (IActionFilter, IAsyncActionFilter, etc.) define contracts in src/Mvc/Mvc.Abstractions/src/Filters/.
  • DefaultFilterProvider discovers and materializes filters from descriptors.
  • FilterFactory orchestrates provider execution, ordering, and caching via CachedReusableFilters.
  • Execution order follows global → controller → action scope, controlled by IOrderedFilter and FilterDescriptorOrderComparer.
  • Reusability eliminates per-request allocation when IsReusable is true.
  • Minimal APIs use IEndpointFilter with RequestDelegateFilterPipelineBuilder for lightweight interception without the full MVC stack.

Frequently Asked Questions

How does ASP.NET Core determine the order of filter execution?

Filters implement IOrderedFilter and specify an Order property (default 0). The FilterDescriptorOrderComparer sorts them by scope—global filters execute first, followed by controller-level, then action-level filters. Within the same scope, lower order values execute before higher values.

What is the difference between IActionFilter and IAsyncActionFilter?

IActionFilter provides synchronous OnActionExecuting and OnActionExecuted methods, while IAsyncActionFilter exposes a single OnActionExecutionAsync method that accepts a delegate to invoke the next filter or action. Async filters prevent thread-blocking during I/O operations and are preferred for modern ASP.NET Core applications.

Can filters be reused across multiple requests to improve performance?

Yes. If a filter implements IFilterFactory and sets IsReusable = true, or if it is a direct instance registered as a singleton, the FilterFactory caches it in CachedReusableFilters on the ActionDescriptor. This prevents reinstantiation on every request, reducing garbage collection pressure.

How do minimal API endpoint filters differ from MVC action filters?

Minimal API endpoint filters implement IEndpointFilter and use RequestDelegateFilterPipelineBuilder to wrap the request delegate in a functional pipeline. Unlike MVC filters, they do not use the ControllerActionInvoker or support the full filter stage pipeline (authorization, resource, exception, result). They provide lightweight interception specifically for the minimal API routing model.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →