Resource filters run early in the pipeline — before model binding — giving them the ability to short-circuit the request before expensive work like JSON deserialization happens. They are useful for response caching, request size validation, and idempotency checks. Authorization filters run before resource and action filters, enforcing access control. Understanding both and how they fit in the full filter order enables you to build precise, efficient cross-cutting concerns that run exactly where they need to in the pipeline.
Resource Filter — Idempotency Key
// ── Resource filter — check idempotency key before model binding ──────────
// Idempotency: a second request with the same key returns the cached first response
// Prevents duplicate order creation, duplicate payments, etc.
public class IdempotencyFilter(IMemoryCache cache) : IAsyncResourceFilter
{
private const string HeaderKey = "Idempotency-Key";
public async Task OnResourceExecutionAsync(
ResourceExecutingContext context,
ResourceExecutionDelegate next)
{
if (!context.HttpContext.Request.Headers.TryGetValue(HeaderKey, out var key))
{
// No idempotency key — proceed normally
await next();
return;
}
var cacheKey = $"idempotency:{key}";
// Check if we have a cached response for this key
if (cache.TryGetValue(cacheKey, out IActionResult? cachedResult))
{
// Return the cached response without executing the action
context.Result = cachedResult;
context.HttpContext.Response.Headers["X-Idempotent-Replayed"] = "true";
return; // short-circuit — action not executed
}
// Execute the action
var executed = await next();
// Cache the result for future duplicate requests (5 minute window)
if (executed.Result is not null)
cache.Set(cacheKey, executed.Result, TimeSpan.FromMinutes(5));
}
}
// ── Register and apply ────────────────────────────────────────────────────
builder.Services.AddMemoryCache();
builder.Services.AddScoped<IdempotencyFilter>();
[ServiceFilter(typeof(IdempotencyFilter))]
[HttpPost("orders")]
public async Task<IActionResult> CreateOrder(CreateOrderRequest request) => Ok();
context.Result, the request body is never read and model binding never runs. This makes resource filters efficient for early-exit scenarios — no JSON deserialization, no DataAnnotations validation, no database queries. The trade-off is that they cannot access the model-bound action parameters since model binding has not happened yet when the filter runs.ResponseCacheAttribute provides — for example, vary by authenticated user, vary by custom headers, or store in a distributed cache with complex invalidation rules. The resource filter caches and serves the result before the action executes, saving the full action execution cost (database queries, business logic). For read-heavy endpoints that do not change frequently, this is one of the highest-leverage performance optimisations.IAuthorizationFilter implementations bypass ASP.NET Core’s full authorisation policy pipeline. They do not integrate with IAuthorizationService or policy evaluation — they are raw, pre-policy checks. Prefer policy-based authorisation ([Authorize(Policy = "...")]) and IAuthorizationHandler for most cases. Use IAuthorizationFilter only for simple, performance-critical checks that cannot be expressed as policies, like API key validation where you want to short-circuit before any other processing.Custom Authorization Filter — API Key
// ── API key authorization filter ──────────────────────────────────────────
public class ApiKeyAuthorizationFilter(IConfiguration configuration) : IAuthorizationFilter
{
private const string ApiKeyHeader = "X-Api-Key";
public void OnAuthorization(AuthorizationFilterContext context)
{
if (!context.HttpContext.Request.Headers.TryGetValue(ApiKeyHeader, out var apiKey))
{
context.Result = new UnauthorizedObjectResult(
new { error = "API key header is missing." });
return;
}
var validKey = configuration["ApiSettings:MasterKey"];
if (!string.Equals(apiKey, validKey, StringComparison.Ordinal))
{
context.Result = new UnauthorizedObjectResult(
new { error = "Invalid API key." });
}
// If key is valid: do nothing — pipeline continues
}
}
// ── Attribute wrapper for clean usage ─────────────────────────────────────
public class RequireApiKeyAttribute : ServiceFilterAttribute
{
public RequireApiKeyAttribute() : base(typeof(ApiKeyAuthorizationFilter)) { }
}
// ── Usage ─────────────────────────────────────────────────────────────────
[RequireApiKey]
[HttpGet("admin/stats")]
public async Task<IActionResult> GetStats() => Ok();
Common Mistakes
Mistake 1 — Accessing action parameters in a resource filter (model binding not done yet)
❌ Wrong — trying to read context.ActionArguments in OnResourceExecuting (empty at this point).
✅ Correct — resource filters run before model binding; use action filters for post-binding access to parameters.
Mistake 2 — Using IAuthorizationFilter instead of policy-based authorisation for complex rules
❌ Wrong — reimplementing claims checking, role checking, and resource-based rules manually in a filter.
✅ Correct — use IAuthorizationHandler with requirements for complex rules; reserve IAuthorizationFilter for simple API key-style checks.