Result filters run around the execution of the action result — after the action method returns its IActionResult, but before and after it is executed (rendered to the response). They are less commonly needed than action filters but useful for modifying results cross-cutting: adding standard response headers, wrapping results in an envelope, or logging response metadata. Exception filters catch unhandled exceptions thrown by action methods and action filters — they provide a per-controller or per-action exception handling layer that is more specific than global middleware exception handling.
Result Filter
// ── Add security response headers to all action results ───────────────────
public class SecurityHeadersResultFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
// Runs just before the result (view, JSON, etc.) is written to response
var response = context.HttpContext.Response;
response.Headers["X-Content-Type-Options"] = "nosniff";
response.Headers["X-Frame-Options"] = "DENY";
response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
}
public void OnResultExecuted(ResultExecutedContext context)
{
// Runs after the result has been written to the response
// Cannot modify headers here — they are already sent
// Can log result metadata
var resultType = context.Result?.GetType().Name ?? "null";
// Log: action completed, result type
}
}
// ── Register globally ─────────────────────────────────────────────────────
builder.Services.AddControllersWithViews(opts =>
{
opts.Filters.Add<SecurityHeadersResultFilter>();
});
app.UseMiddleware<SecurityHeadersMiddleware>) instead of a result filter — middleware runs for all responses, filters only for successful action results.NotFoundException → 404, ConflictException → 409, UnauthorisedAccessException → 403. This eliminates repetitive try/catch blocks from every action method and ensures consistent error responses across all actions.app.UseExceptionHandler() or a global IExceptionHandler in addition to exception filters. Both layers complement each other.Exception Filter
// ── Exception filter — maps domain exceptions to HTTP responses ────────────
public class DomainExceptionFilter(ILogger<DomainExceptionFilter> logger) : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
// Map domain exceptions to HTTP status codes
context.Result = context.Exception switch
{
NotFoundException ex =>
new NotFoundObjectResult(new { error = ex.Message }),
ConflictException ex =>
new ConflictObjectResult(new { error = ex.Message }),
ForbiddenException =>
new ForbidResult(),
ValidationException ex =>
new BadRequestObjectResult(new
{
error = "Validation failed.",
errors = ex.Errors,
}),
_ => null // null = not handled — let it propagate
};
if (context.Result is not null)
{
// Mark as handled so it does not propagate further
context.ExceptionHandled = true;
logger.LogWarning(context.Exception,
"Domain exception handled by filter: {ExceptionType}",
context.Exception.GetType().Name);
}
else
{
// Unhandled exception — let global handler deal with it
logger.LogError(context.Exception, "Unhandled exception in action.");
}
}
}
// ── Apply to all controllers via base controller ──────────────────────────
[ServiceFilter(typeof(DomainExceptionFilter))]
public abstract class BaseController : Controller { }
builder.Services.AddScoped<DomainExceptionFilter>();
Common Mistakes
Mistake 1 — Not setting context.ExceptionHandled = true (exception re-thrown)
❌ Wrong — setting context.Result without context.ExceptionHandled; exception propagates to global handler as well:
context.Result = new NotFoundResult();
// forgot: context.ExceptionHandled = true — exception still propagates!
✅ Correct — always set both context.Result and context.ExceptionHandled = true when handling an exception.
Mistake 2 — Using exception filters for exceptions from view rendering
❌ Wrong — exception filter cannot catch exceptions thrown during Razor view compilation or rendering.
✅ Correct — use UseExceptionHandler middleware for comprehensive coverage including view rendering errors.