ASP.NET Core 6 Filters and Request Pipeline Model

1. Middleware Components

Middleware components form the foundation of the request processing pipeline in ASP.NET Core applications. Each middleware component gets invoked sequentially, and you have complete control over their arrangement and custom middleware injection points.

Key middleware considerations:

  • ExceptionHandler Middleware: Catches exceptions that occur during request processing
  • CORS Middleware: Should not be reordered with other middleware as it may cause unexpected behavior

Middleware components are registered in Program.cs using extension methods like app.Use().

2. Filter Pipeline Overview

Filters implement Aspect-Oriented Programming (AOP) principles, allowing you to inject cross-cutting concerns such as authentication, logging, and validation before, during, or after controller action execution. They execute after the middleware pipeline completes.

The filter pipeline consists of five distinct filter types that execute in a specific sequence:

  • Authorization Filters: Perform authentication and authorization checks
  • Resource Filters: Handle caching and early request termination
  • Action Filters: Execute validation and logging around action execution
  • Exception Filters: Handle unhandled exceptions
  • Result Filters: Process action results before and after response generation

Filter Type Reference

Implementing an Action Filter

Custom filters inherit from the appropriate interface and extend the Attribute class for use as decorators:

namespace Application.Filters
{
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
    public class TimingActionFilterAttribute : Attribute, IActionFilter
    {
        public void OnActionExecuted(ActionExecutedContext context)
        {
            var stopwatch = Stopwatch.StartNew();
            context.HttpContext.Items["execution_time"] = stopwatch.ElapsedMilliseconds;
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<TimingActionFilterAttribute>>();
            logger.LogInformation($"Executing {context.ActionDescriptor.DisplayName}");
        }
    }
}

Filter Registration Strategies

1. Method-Level Registration

Apply filters directly to individual action methods for granular control:

[HttpGet]
[TimingActionFilter]
public IEnumerable<WeatherForecast> Get()
{
    return _weatherForecastService.GetForecast();
}

2. Controller-Level Registration

Register filters at the controller level to apply them to all actions within that controller. Filters applied to a controller execute before method-level filters of the same type:

[ApiController]
[Route("api/[controller]")]
[TimingActionFilter]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Empty<WeatherForecast>();
    }
}

3. Global Registration

Register filters globally in Program.cs to apply them to all controllers and actions:

builder.Services.AddControllers(opt =>
{
    opt.Filters.Add<TimingActionFilterAttribute>();
    opt.Filters.Add<ValidationActionFilterAttribute>();
});

4. Controlling Execution Order

When registering filters globally, you can specify execution order using integer values. Lower values execute first:

builder.Services.AddControllers(opt =>
{
    opt.Filters.Add<GlobalLoggingFilterAttribute>(1);
    opt.Filters.Add<GlobalErrorHandlerFilterAttribute>(2);
});

Dependency Injection with Filters

Using TypeFilter for On-Demand Resolution

TypeFilter creates filter instances dynamically without requiring the filter type to be registered in the DI container:

[HttpGet]
[TypeFilter(typeof(AuditLogFilterAttribute))]
public IActionResult GetData(int id)
{
    return Ok(_repository.GetById(id));
}

Using ServiceFilter for DI-Registered Types

ServiceFilter resolves filters from the DI container, making it suitable for filters requiring constructor injection:

// Program.cs - Container registration
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(container =>
{
    container.RegisterType<AuditLogFilterAttribute>().AsSelf().InstancePerLifetimeScope();
    container.RegisterType<UserActivityRepository>().AsImplementedInterfaces();
});

// Controller usage
[HttpGet]
[ServiceFilter(typeof(AuditLogFilterAttribute))]
public IActionResult GetUserActivity(int userId)
{
    return Ok(_activityService.GetUserActivities(userId));
}

Constructor Injection Example

public class AuditLogFilterAttribute : Attribute, IActionFilter
{
    private readonly IAuditService _auditService;
    private readonly ILogger<AuditLogFilterAttribute> _logger;

    public AuditLogFilterAttribute(IAuditService auditService, ILogger<AuditLogFilterAttribute> logger)
    {
        _auditService = auditService;
        _logger = logger;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        var userId = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        _auditService.LogAccess(userId, context.ActionDescriptor.DisplayName);
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        _logger.LogInformation($"Action {context.ActionDescriptor.DisplayName} completed with result: {context.Result}");
    }
}

Authorization Filter Deep Dive

Role-Based Access Control

The authorization filter is the first filter in the pipeline, executing before model binding occurs. This means you cannot access action parameters through the context:

namespace Application.Filters
{
    public class AuthorizationFilterAttribute : Attribute, IAuthorizationFilter
    {
        private readonly IUserPermissionService _permissionService;
        private readonly ILogger<AuthorizationFilterAttribute> _logger;

        public AuthorizationFilterAttribute(IUserPermissionService permissionService, ILogger<AuthorizationFilterAttribute> logger)
        {
            _permissionService = permissionService;
            _logger = logger;
        }

        public void OnAuthorization(AuthorizationFilterContext context)
        {
            var routeData = context.RouteData;
            var controllerName = routeData.Values["controller"]?.ToString();
            var actionName = routeData.Values["action"]?.ToString();

            var userIdClaim = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
            if (userIdClaim == null)
            {
                context.Result = new UnauthorizedResult();
                return;
            }

            int userId = int.Parse(userIdClaim.Value);
            var requiredPermissions = _permissionService.GetRequiredPermissions(controllerName, actionName);
            var userPermissions = _permissionService.GetUserPermissions(userId);

            if (!requiredPermissions.All(p => userPermissions.Contains(p)))
            {
                _logger.LogWarning("User {UserId} denied access to {Controller}/{Action}", userId, controllerName, actionName);
                context.Result = new ForbidResult();
            }
        }
    }
}

Resource Filters and Short-Circuiting

Caching Implementation

Resource filters can short-circuit the request pipeline by setting the Result property, preventing subsequent filters and the action from executing:

namespace Application.Filters
{
    public class ResponseCacheFilterAttribute : Attribute, IResourceFilter
    {
        private readonly IMemoryCache _cache;
        private readonly ILogger<ResponseCacheFilterAttribute> _logger;
        private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(60);

        public ResponseCacheFilterAttribute(IMemoryCache cache, ILogger<ResponseCacheFilterAttribute> logger)
        {
            _cache = cache;
            _logger = logger;
        }

        public void OnResourceExecuting(ResourceExecutingContext context)
        {
            var cacheKey = GenerateCacheKey(context.HttpContext.Request);
            
            if (_cache.TryGetValue(cacheKey, out var cachedResponse))
            {
                _logger.LogInformation("Cache hit for key: {CacheKey}", cacheKey);
                context.Result = cachedResponse as IActionResult;
                return;
            }

            _logger.LogInformation("Cache miss for key: {CacheKey}", cacheKey);
        }

        public void OnResourceExecuted(ResourceExecutedContext context)
        {
            var cacheKey = GenerateCacheKey(context.HttpContext.Request);
            var response = context.Result;
            
            _cache.Set(cacheKey, response, CacheDuration);
            _logger.LogInformation("Cached response for key: {CacheKey}", cacheKey);
        }

        private static string GenerateCacheKey(HttpRequest request)
        {
            return $"response:{request.Path}:{request.QueryString}";
        }
    }
}

DI Container Configuration for Caching

// Program.cs
builder.Services.AddMemoryCache(opt =>
{
    opt.ExpirationScanFrequency = TimeSpan.FromMinutes(5);
    opt.SizeLimit = 1000;
});

Action Filters for Validation and Logging

Comprehensive Request Logging

namespace Application.Filters
{
    public class RequestLoggingFilterAttribute : Attribute, IActionFilter
    {
        private readonly ILogger<RequestLoggingFilterAttribute> _logger;
        private readonly Stopwatch _stopwatch;

        public RequestLoggingFilterAttribute(ILogger<RequestLoggingFilterAttribute> logger)
        {
            _logger = logger;
            _stopwatch = new Stopwatch();
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            _stopwatch.Start();
            
            var requestId = context.HttpContext.TraceIdentifier;
            var actionName = context.ActionDescriptor.DisplayName;
            
            context.HttpContext.Items["request_start_time"] = DateTime.UtcNow;
            
            _logger.LogInformation("Request {RequestId} starting: {ActionName} with parameters: {@Parameters}",
                requestId, actionName, context.ActionArguments);
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            _stopwatch.Stop();
            var requestId = context.HttpContext.TraceIdentifier;
            
            _logger.LogInformation("Request {RequestId} completed in {ElapsedMs}ms with result: {Result}",
                requestId, _stopwatch.ElapsedMilliseconds, context.Result);
        }
    }
}

Model State Validation

public class ModelValidationFilterAttribute : Attribute, IActionFilter
{
    private readonly ILogger<ModelValidationFilterAttribute> _logger;

    public ModelValidationFilterAttribute(ILogger<ModelValidationFilterAttribute> logger)
    {
        _logger = logger;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var errors = context.ModelState
                .Where(e => e.Value?.Errors.Count > 0)
                .ToDictionary(
                    kvp => kvp.Key,
                    kvp => kvp.Value!.Errors.Select(e => e.ErrorMessage).ToArray()
                );
            
            _logger.LogWarning("Model validation failed: {@ValidationErrors}", errors);
            context.Result = new BadRequestObjectResult(new { Errors = errors });
        }
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Post-execution logic if needed
    }
}

Exception Handling with Filters

Centralized Expection Handler

namespace Application.Filters
{
    public class GlobalExceptionFilterAttribute : Attribute, IExceptionFilter
    {
        private readonly ILogger<GlobalExceptionFilterAttribute> _logger;
        private readonly IHostEnvironment _environment;

        public GlobalExceptionFilterAttribute(ILogger<GlobalExceptionFilterAttribute> logger, IHostEnvironment environment)
        {
            _logger = logger;
            _environment = environment;
        }

        public void OnException(ExceptionContext context)
        {
            var requestId = context.HttpContext.TraceIdentifier;
            
            _logger.LogError(context.Exception, "Request {RequestId} encountered an unhandled exception", requestId);

            var errorResponse = new ErrorResponse
            {
                RequestId = requestId,
                Message = _environment.IsDevelopment() ? context.Exception.Message : "An unexpected error occurred",
                Timestamp = DateTime.UtcNow
            };

            if (context.Exception is ArgumentException argException)
            {
                context.Result = new BadRequestObjectResult(errorResponse);
            }
            else if (context.Exception is UnauthorizedAccessException)
            {
                context.Result = new UnauthorizedObjectResult(errorResponse);
            }
            else
            {
                context.Result = new ObjectResult(errorResponse)
                {
                    StatusCode = StatusCodes.Status500InternalServerError
                };
            }
        }
    }

    public class ErrorResponse
    {
        public string RequestId { get; set; } = string.Empty;
        public string Message { get; set; } = string.Empty;
        public DateTime Timestamp { get; set; }
    }
}

Usage Example

[HttpGet]
[TypeFilter(typeof(GlobalExceptionFilterAttribute))]
public IActionResult ThrowingEndpoint()
{
    throw new InvalidOperationException("This demonstrates exception filter behavior");
}

Result Filters for Response Processing

Response Modification

namespace Application.Filters
{
    public class ResponseEnrichmentFilterAttribute : Attribute, IResultFilter
    {
        private readonly ICurrentUserService _currentUserService;

        public ResponseEnrichmentFilterAttribute(ICurrentUserService currentUserService)
        {
            _currentUserService = currentUserService;
        }

        public void OnResultExecuting(ResultExecutingContext context)
        {
            if (context.Result is ObjectResult objectResult)
            {
                var userId = _currentUserService.GetCurrentUserId();
                objectResult.Value = new EnrichedResponse
                {
                    Data = objectResult.Value,
                    Metadata = new ResponseMetadata
                    {
                        GeneratedAt = DateTime.UtcNow,
                        GeneratedBy = userId
                    }
                };
            }
        }

        public void OnResultExecuted(ResultExecutedContext context)
        {
            // Cleanup or post-processing after result execution
        }
    }

    public class EnrichedResponse
    {
        public object? Data { get; set; }
        public ResponseMetadata Metadata { get; set; } = new();
    }

    public class ResponseMetadata
    {
        public DateTime GeneratedAt { get; set; }
        public int GeneratedBy { get; set; }
    }
}

Always-Run Result Filters

IAlwaysRunResultFilter executes regardless of whether earlier filters short-circuit the pipeline:

public class FallbackResultFilterAttribute : Attribute, IAlwaysRunResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.Result is StatusCodeResult statusCode)
        {
            if (statusCode.StatusCode == StatusCodes.Status404NotFound)
            {
                context.Result = new ObjectResult(new
                {
                    Message = "The requested resource was not found",
                    StatusCode = StatusCodes.Status404NotFound
                })
                {
                    StatusCode = StatusCodes.Status404NotFound
                };
            }
            else if (statusCode.StatusCode == StatusCodes.Status403Forbidden)
            {
                context.Result = new ObjectResult(new
                {
                    Message = "Access to this resource is forbidden",
                    StatusCode = StatusCodes.Status403Forbidden
                })
                {
                    StatusCode = StatusCodes.Status403Forbidden
                };
            }
        }
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        // Post-result execution logic
    }
}

Filter Execution Order Summary

Pipeline Sequence

When filter are applied at multiple scopes, the execution order follows this pattern:

  1. Global filters (first)
  2. Controller-level filters
  3. Method-level filters (last)

Configuring Order via Attribtues

public class OrderedFilterAttribute : Attribute, IActionFilter
{
    // Implementation details
}

// Controller with ordered filters
[ServiceFilter(typeof(OrderedFilterAttribute), Order = 2)]
public class SampleController : ControllerBase
{
    [ServiceFilter(typeof(OrderedFilterAttribute), Order = 1)]
    public IActionResult Action()
    {
        return Ok();
    }
}

Program.cs Global Order Configuration

builder.Services.AddControllers(opt =>
{
    // Lower Order value = earlier execution
    opt.Filters.Add<FirstFilter>(1);
    opt.Filters.Add<SecondFilter>(2);
    opt.Filters.Add<ThirdFilter>(3);
});

Order of Execution Within Same Filter Type

For filters of the same type applied at different scopes:

  • All global filters execute before controller-level filters
  • Controller-level filters execute before method-level filters
  • When multiple filters have the same scope, the Order property determines precedence

Tags: aspnetcore dotnet6 middleware Filter Pipeline

Posted on Fri, 06 Nov 2026 16:17:15 +0000 by darkerstar