← Back to Articles         
ASP.NET Core Resource Filters

ASP.NET Core Resource Filters

Published on 26 Sep 2026     6 min read Web API
Filters

Resource Filter in ASP.NET Core Web API

What is a resource filter?

A resource filter runs around a large part of the controller action pipeline. It runs after authorization and before model binding. Its code can run again after the remaining pipeline finishes. Microsoft Learn

In simple terms:

A resource filter can examine a request early, allow the controller action to run, and then do work when that processing finishes.

It is especially useful when you want to stop a request before model binding and action execution. A cache hit is one example: the filter can return a cached result without running the action again. Microsoft Learn

Its place in the execution order

Request
  ↓
Authorization
  ↓
Resource filter: before
  ↓
Model binding
  ↓
Action filter: before
  ↓
Controller action
  ↓
Action filter: after
  ↓
Result filter and result execution
  ↓
Resource filter: after
  ↓
Response

A resource filter has two main forms:

Synchronous Asynchronous When it runs
OnResourceExecuting Code before await next() Before model binding and the remaining controller pipeline
OnResourceExecuted Code after await next() After the remaining controller pipeline finishes

Implement IResourceFilter for the synchronous methods or IAsyncResourceFilter for the asynchronous method. The following complete example uses the asynchronous form so you can clearly see the “before → action → after” flow. Microsoft Learn

Full working example

We will create this endpoint:

GET /api/products/1

The resource filter will:

  1. Check for an X-Client-Id request header.
  2. Return 400 Bad Request immediately if it is missing.
  3. Allow the controller action to run if it is present.
  4. Log a message before and after the remaining pipeline.

This header is just a demo input requirement. It is not an authentication or security check.

Step 1: Create a controller-based Web API

dotnet new webapi --use-controllers -n ResourceFilterDemo
cd ResourceFilterDemo

No additional NuGet package is required for this example.

Step 2: Create Filters/ClientIdResourceFilter.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace ResourceFilterDemo.Filters;

public sealed class ClientIdResourceFilter : IAsyncResourceFilter
{
    private readonly ILogger<ClientIdResourceFilter> _logger;

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

    public async Task OnResourceExecutionAsync(
        ResourceExecutingContext context,
        ResourceExecutionDelegate next)
    {
        // BEFORE model binding and the controller action
        _logger.LogInformation(
            "RESOURCE FILTER BEFORE: Checking X-Client-Id.");

        if (!context.HttpContext.Request.Headers.TryGetValue(
                "X-Client-Id",
                out var clientId) ||
            string.IsNullOrWhiteSpace(clientId.ToString()))
        {
            _logger.LogInformation(
                "RESOURCE FILTER: Header missing. Stopping request.");

            context.Result = new BadRequestObjectResult(
                new { message = "X-Client-Id header is required." });

            // No next(): model binding and the controller action are skipped.
            return;
        }

        _logger.LogInformation(
            "RESOURCE FILTER: Client ID is {ClientId}.",
            clientId.ToString());

        // Run the rest of the controller filter pipeline.
        ResourceExecutedContext executedContext = await next();

        // AFTER the remaining controller pipeline finishes
        _logger.LogInformation(
            "RESOURCE FILTER AFTER: Pipeline finished. Canceled: {Canceled}.",
            executedContext.Canceled);
    }
}

Step 3: Create Controllers/ProductsController.cs

using Microsoft.AspNetCore.Mvc;
using ResourceFilterDemo.Filters;

namespace ResourceFilterDemo.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ILogger<ProductsController> _logger;

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

    [HttpGet("{id:int}")]
    [ServiceFilter(typeof(ClientIdResourceFilter))]
    public IActionResult GetById(int id)
    {
        _logger.LogInformation(
            "CONTROLLER ACTION: Getting product {ProductId}.",
            id);

        var products = new[]
        {
            new Product(1, "Laptop", 55000),
            new Product(2, "Mouse", 700)
        };

        var product = products.FirstOrDefault(p => p.Id == id);

        if (product is null)
        {
            return NotFound(
                new { message = $"Product {id} was not found." });
        }

        return Ok(product);
    }
}

public record Product(int Id, string Name, decimal Price);

[ServiceFilter(typeof(ClientIdResourceFilter))] applies the filter to this action and lets ASP.NET Core create it through dependency injection. That is how its ILogger<ClientIdResourceFilter> constructor parameter is supplied.

Step 4: Register the filter in Program.cs

using ResourceFilterDemo.Filters;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

// Required because the action uses [ServiceFilter].
builder.Services.AddScoped<ClientIdResourceFilter>();

var app = builder.Build();

app.MapControllers();

app.Run();

The essential registration is:

builder.Services.AddScoped<ClientIdResourceFilter>();

Without it, [ServiceFilter] cannot resolve the filter from dependency injection.

Step 5: Run and test

dotnet run

Use the URL shown in your terminal or Visual Studio.

Test A: Header is present

GET /api/products/1
X-Client-Id: syed-demo

Response: 200 OK

{
  "id": 1,
  "name": "Laptop",
  "price": 55000
}

Console log order:

RESOURCE FILTER BEFORE: Checking X-Client-Id.
RESOURCE FILTER: Client ID is syed-demo.
CONTROLLER ACTION: Getting product 1.
RESOURCE FILTER AFTER: Pipeline finished. Canceled: False.

The important line is await next(): it lets the request proceed to model binding and the action. Once that work finishes, execution returns to the resource filter.

Test B: Header is missing

GET /api/products/1

Response: 400 Bad Request

{
  "message": "X-Client-Id header is required."
}

Console log order:

RESOURCE FILTER BEFORE: Checking X-Client-Id.
RESOURCE FILTER: Header missing. Stopping request.

There is no controller action log and no “RESOURCE FILTER AFTER” log. The filter set context.Result and returned without calling next(), so the remaining controller pipeline was skipped. Setting Result is how a resource filter short-circuits that pipeline. Microsoft Learn

Code explanation

IAsyncResourceFilter

public sealed class ClientIdResourceFilter : IAsyncResourceFilter

This tells ASP.NET Core that the class is an asynchronous resource filter. Its method receives the current request context and a next delegate.

ResourceExecutingContext context

ResourceExecutingContext context

This gives the filter access to information about the current request. In the example, we read the request header through:

context.HttpContext.Request.Headers

We can also assign an action result to:

context.Result

A non-null Result stops the remaining resource and action pipeline and uses that result for the response. Microsoft Learn

ResourceExecutionDelegate next

ResourceExecutionDelegate next

Calling:

await next();

means “continue running the rest of the controller pipeline.” The action can then execute, followed by its result. When that work finishes, execution returns to the line after await next().

ResourceExecutedContext executedContext

ResourceExecutedContext executedContext = await next();

This provides information about how the remaining pipeline finished. For example, its Canceled property indicates that a resource filter short-circuited execution. The after section is also a suitable place for observing completion or cleaning up work started by this filter. Microsoft Learn

When would you use a resource filter?

Common examples include:

  • Returning a cached result early, avoiding model binding and action execution.
  • Rejecting a request before model binding when a required request condition is missing.
  • Changing model-binding behavior, such as preventing form data from being read automatically for a large file upload. Microsoft Learn

For normal application authorization, continue using [Authorize] and policies. For general work on every HTTP request, middleware is often a better fit.

Key points for interviews

  • A resource filter runs after authorization and before model binding.
  • It surrounds model binding, the action, and result processing.
  • OnResourceExecuting runs before; OnResourceExecuted runs afterward.
  • In IAsyncResourceFilter, code before await next() is the “before” part; code after it is the “after” part.
  • Setting context.Result and not calling next() stops the remaining controller pipeline.
  • If the filter itself short-circuits before next(), its code after next() cannot run because next() was never called.
← Back to Articles