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:
- Global filters (first)
- Controller-level filters
- 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
Orderproperty determines precedence