Redigera

Dela via


Output caching middleware in ASP.NET Core

By Tom Dykstra

Note

This isn't the latest version of this article. For the current release, see the .NET 8 version of this article.

Warning

This version of ASP.NET Core is no longer supported. For more information, see .NET and .NET Core Support Policy. For the current release, see the .NET 8 version of this article.

Important

This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.

For the current release, see the .NET 8 version of this article.

This article explains how to configure output caching middleware in an ASP.NET Core app. For an introduction to output caching, see Output caching.

The output caching middleware can be used in all types of ASP.NET Core apps: Minimal API, Web API with controllers, MVC, and Razor Pages. Code examples are provided for minimal APIs and controller-based APIs. The controller-based API examples show how to use attributes to configure caching. These attributes can also be used in MVC and Razor Pages apps.

The code examples refer to a Gravatar class that generates an image and provides a "generated at" date and time. The class is defined and used only in the sample app. Its purpose is to make it easy to see when cached output is being used. For more information, see How to download a sample and Preprocessor directives in sample code.

Add the middleware to the app

Add the output caching middleware to the service collection by calling AddOutputCache.

Add the middleware to the request processing pipeline by calling UseOutputCache.

For example:

builder.Services.AddOutputCache();
var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseOutputCache();
app.UseAuthorization();

Calling AddOutputCacheand UseOutputCache doesn't start caching behavior, it makes caching available. To make the app cache responses, caching must be configured as shown in the following sections.

Note

  • In apps that use CORS middleware, UseOutputCache must be called after UseCors.
  • In Razor Pages apps and apps with controllers, UseOutputCache must be called after UseRouting.

Configure one endpoint or page

For minimal API apps, configure an endpoint to do caching by calling CacheOutput, or by applying the [OutputCache] attribute, as shown in the following examples:

app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) => 
    Gravatar.WriteGravatar(context));

For apps with controllers, apply the [OutputCache] attribute to the action method as shown here:

[ApiController]
[Route("/[controller]")]
[OutputCache]
public class CachedController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

For Razor Pages apps, apply the attribute to the Razor page class.

Configure multiple endpoints or pages

Create policies when calling AddOutputCache to specify caching configuration that applies to multiple endpoints. A policy can be selected for specific endpoints, while a base policy provides default caching configuration for a collection of endpoints.

The following highlighted code configures caching for all of the app's endpoints, with expiration time of 10 seconds. If an expiration time isn't specified, it defaults to one minute.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

The following highlighted code creates two policies, each specifying a different expiration time. Selected endpoints can use the 20-second expiration, and others can use the 30-second expiration.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

You can select a policy for an endpoint when calling the CacheOutput method or using the [OutputCache] attribute.

In a minimal API app, the following code configures one endpoint with a 20-second expiration and one with a 30-second expiration:

app.MapGet("/20", Gravatar.WriteGravatar).CacheOutput("Expire20");
app.MapGet("/30", [OutputCache(PolicyName = "Expire30")] (context) => 
    Gravatar.WriteGravatar(context));

For apps with controllers, apply the [OutputCache] attribute to the action method to select a policy:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Expire20")]
public class Expire20Controller : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

For Razor Pages apps, apply the attribute to the Razor page class.

Default output caching policy

By default, output caching follows these rules:

  • Only HTTP 200 responses are cached.
  • Only HTTP GET or HEAD requests are cached.
  • Responses that set cookies aren't cached.
  • Responses to authenticated requests aren't cached.

The following code applies all of the default caching rules to all of an app's endpoints:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Cache());
});

Override the default policy

The following code shows how to override the default rules. The highlighted lines in the following custom policy code enable caching for HTTP POST methods and HTTP 301 responses:

using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Primitives;

namespace OCMinimal;

public sealed class MyCustomPolicy : IOutputCachePolicy
{
    public static readonly MyCustomPolicy Instance = new();

    private MyCustomPolicy()
    {
    }

    ValueTask IOutputCachePolicy.CacheRequestAsync(
        OutputCacheContext context, 
        CancellationToken cancellationToken)
    {
        var attemptOutputCaching = AttemptOutputCaching(context);
        context.EnableOutputCaching = true;
        context.AllowCacheLookup = attemptOutputCaching;
        context.AllowCacheStorage = attemptOutputCaching;
        context.AllowLocking = true;

        // Vary by any query by default
        context.CacheVaryByRules.QueryKeys = "*";

        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeFromCacheAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeResponseAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        var response = context.HttpContext.Response;

        // Verify existence of cookie headers
        if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        // Check response code
        if (response.StatusCode != StatusCodes.Status200OK && 
            response.StatusCode != StatusCodes.Status301MovedPermanently)
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        return ValueTask.CompletedTask;
    }

    private static bool AttemptOutputCaching(OutputCacheContext context)
    {
        // Check if the current request fulfills the requirements
        // to be cached
        var request = context.HttpContext.Request;

        // Verify the method
        if (!HttpMethods.IsGet(request.Method) && 
            !HttpMethods.IsHead(request.Method) && 
            !HttpMethods.IsPost(request.Method))
        {
            return false;
        }

        // Verify existence of authorization headers
        if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || 
            request.HttpContext.User?.Identity?.IsAuthenticated == true)
        {
            return false;
        }

        return true;
    }
}

To use this custom policy, create a named policy:

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", MyCustomPolicy.Instance);
});

And select the named policy for an endpoint. The following code selects the custom policy for an endpoint in a minimal API app:

app.MapPost("/cachedpost", Gravatar.WriteGravatar)
    .CacheOutput("CachePost");

The following code does the same for a controller action:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "CachePost")]
public class PostController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Alternative default policy override

Alternatively, use Dependency Injection (DI) to initialize an instance, with the following changes to the custom policy class:

  • A public constructor instead of a private constructor.
  • Eliminate the Instance property in the custom policy class.

For example:

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

The remainder of the class is the same as shown previously. Add the custom policy as shown in the following example:

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", builder => 
        builder.AddPolicy<MyCustomPolicy2>(), true);
});

The preceding code uses DI to create the instance of the custom policy class. Any public arguments in the constructor are resolved.

When using a custom policy as a base policy, don't call OutputCache() (with no arguments) or use the [OutputCache] attribute on any endpoint that the base policy should apply to. Calling OutputCache() or using the attribute adds the default policy to the endpoint.

Specify the cache key

By default, every part of the URL is included as the key to a cache entry, that is, the scheme, host, port, path, and query string. However, you might want to explicitly control the cache key. For example, suppose you have an endpoint that returns a unique response only for each unique value of the culture query string. Variation in other parts of the URL, such as other query strings, shouldn't result in different cache entries. You can specify such rules in a policy, as shown in the following highlighted code:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

You can then select the VaryByQuery policy for an endpoint. In a minimal API app, the following code selects the VaryByQuery policy for an endpoint that returns a unique response only for each unique value of the culture query string:

app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput("Query");

The following code does the same for a controller action:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Query")]
public class QueryController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Here are some of the options for controlling the cache key:

  • SetVaryByQuery - Specify one or more query string names to add to the cache key.

  • SetVaryByHeader - Specify one or more HTTP headers to add to the cache key.

  • VaryByValue- Specify a value to add to the cache key. The following example uses a value that indicates whether the current server time in seconds is odd or even. A new response is generated only when the number of seconds goes from odd to even or even to odd.

    builder.Services.AddOutputCache(options =>
    {
        options.AddBasePolicy(builder => builder
            .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
            .Tag("tag-blog"));
        options.AddBasePolicy(builder => builder.Tag("tag-all"));
        options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
        options.AddPolicy("NoCache", builder => builder.NoCache());
        options.AddPolicy("NoLock", builder => builder.SetLocking(false));
        options.AddPolicy("VaryByValue", builder => 
            builder.VaryByValue((context) =>
                new KeyValuePair<string, string>(
                "time", (DateTime.Now.Second % 2)
                    .ToString(CultureInfo.InvariantCulture))));
    });
    

Use OutputCacheOptions.UseCaseSensitivePaths to specify that the path part of the key is case sensitive. The default is case insensitive.

For more options, see the OutputCachePolicyBuilder class.

Cache revalidation

Cache revalidation means the server can return a 304 Not Modified HTTP status code instead of the full response body. This status code informs the client that the response to the request is unchanged from what the client previously received.

The following code illustrates the use of an Etag header to enable cache revalidation. If the client sends an If-None-Match header with the etag value of an earlier response, and the cache entry is fresh, the server returns 304 Not Modified instead of the full response. Here's how to set the etag value in a policy, in a Minimal API app:

app.MapGet("/etag", async (context) =>
{
    var etag = $"\"{Guid.NewGuid():n}\"";
    context.Response.Headers.ETag = etag;
    await Gravatar.WriteGravatar(context);

}).CacheOutput();

And here's how to set the etag value in a controller-based API:

[ApiController]
[Route("/[controller]")]
[OutputCache]
public class EtagController : ControllerBase
{
    public async Task GetAsync()
    {
        var etag = $"\"{Guid.NewGuid():n}\"";
        HttpContext.Response.Headers.ETag = etag;
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Another way to do cache revalidation is to check the date of the cache entry creation compared to the date requested by the client. When the request header If-Modified-Since is provided, output caching returns 304 if the cached entry is older and isn't expired.

Cache revalidation is automatic in response to these headers sent from the client. No special configuration is required on the server to enable this behavior, aside from enabling output caching.

Use tags to evict cache entries

You can use tags to identify a group of endpoints and evict all cache entries for the group. For example, the following minimal API code creates a pair of endpoints whose URLs begin with "blog", and tags them "tag-blog":

app.MapGet("/blog", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));
app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));

The following code shows how to assign tags to an endpoint in a controller-based API:

[ApiController]
[Route("/[controller]")]
[OutputCache(Tags = new[] { "tag-blog", "tag-all" })]
public class TagEndpointController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

An alternative way to assign tags for endpoints with routes that begin with blog is to define a base policy that applies to all endpoints with that route. The following code shows how to do that:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Another alternative for minimal API apps is to call MapGroup:

var blog = app.MapGroup("blog")
    .CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);

In the preceding tag assignment examples, both endpoints are identified by the tag-blog tag. You can then evict the cache entries for those endpoints with a single statement that references that tag:

app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) =>
{
    await cache.EvictByTagAsync(tag, default);
});

With this code, an HTTP POST request sent to https://localhost:<port>/purge/tag-blog evicts cache entries for these endpoints.

You might want a way to evict all cache entries for all endpoints. To do that, create a base policy for all endpoints as the following code does:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

This base policy enables you to use the "tag-all" tag to evict everything in cache.

Disable resource locking

By default, resource locking is enabled to mitigate the risk of cache stampede and thundering herd. For more information, see Output Caching.

To disable resource locking, call SetLocking(false) while creating a policy, as shown in the following example:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

The following example selects the no-locking policy for an endpoint in a minimal API app:

app.MapGet("/nolock", Gravatar.WriteGravatar)
    .CacheOutput("NoLock");

In a controller-based API, use the attribute to select the policy:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "NoLock")]
public class NoLockController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Limits

The following properties of OutputCacheOptions let you configure limits that apply to all endpoints:

  • SizeLimit - Maximum size of cache storage. When this limit is reached, no new responses are cached until older entries are evicted. Default value is 100 MB.
  • MaximumBodySize - If the response body exceeds this limit, it isn't cached. Default value is 64 MB.
  • DefaultExpirationTimeSpan - The expiration time duration that applies when not specified by a policy. Default value is 60 seconds.

Cache storage

IOutputCacheStore is used for storage. By default it's used with MemoryCache. Cached responses are stored in-process, so each server has a separate cache that is lost whenever the server process is restarted.

Redis cache

An alternative is to use Redis cache. Redis cache provides consistency between server nodes via a shared cache that outlives individual server processes. To use Redis for output caching:

  • Install the Microsoft.AspNetCore.OutputCaching.StackExchangeRedis NuGet package.

  • Call builder.Services.AddStackExchangeRedisOutputCache (not AddStackExchangeRedisCache), and provide a connection string that points to a Redis server.

    For example:

    builder.Services.AddStackExchangeRedisOutputCache(options =>
    {
        options.Configuration = 
            builder.Configuration.GetConnectionString("MyRedisConStr");
        options.InstanceName = "SampleInstance";
    });
    
    builder.Services.AddOutputCache(options =>
    {
        options.AddBasePolicy(builder => 
            builder.Expire(TimeSpan.FromSeconds(10)));
    });
    
    • options.Configuration - A connection string to an on-premises Redis server or to a hosted offering such as Azure Cache for Redis. For example, <instance_name>.redis.cache.windows.net:6380,password=<password>,ssl=True,abortConnect=False for Azure cache for Redis.
    • options.InstanceName - Optional, specifies a logical partition for the cache.

    The configuration options are identical to the Redis-based distributed caching options.

We don't recommend IDistributedCache for use with output caching. IDistributedCache doesn't have atomic features, which are required for tagging. We recommend that you use the built-in support for Redis or create custom IOutputCacheStore implementations by using direct dependencies on the underlying storage mechanism.

See also

This article explains how to configure output caching middleware in an ASP.NET Core app. For an introduction to output caching, see Output caching.

The output caching middleware can be used in all types of ASP.NET Core apps: Minimal API, Web API with controllers, MVC, and Razor Pages. The sample app is a Minimal API, but every caching feature it illustrates is also supported in the other app types.

Add the middleware to the app

Add the output caching middleware to the service collection by calling AddOutputCache.

Add the middleware to the request processing pipeline by calling UseOutputCache.

Note

  • In apps that use CORS middleware, UseOutputCache must be called after UseCors.
  • In Razor Pages apps and apps with controllers, UseOutputCache must be called after UseRouting.
  • Calling AddOutputCacheand UseOutputCache doesn't start caching behavior, it makes caching available. Caching response data must be configured as shown in the following sections.

Configure one endpoint or page

For minimal API apps, configure an endpoint to do caching by calling CacheOutput, or by applying the [OutputCache] attribute, as shown in the following examples:

app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) => 
    Gravatar.WriteGravatar(context));

For apps with controllers, apply the [OutputCache] attribute to the action method. For Razor Pages apps, apply the attribute to the Razor page class.

Configure multiple endpoints or pages

Create policies when calling AddOutputCache to specify caching configuration that applies to multiple endpoints. A policy can be selected for specific endpoints, while a base policy provides default caching configuration for a collection of endpoints.

The following highlighted code configures caching for all of the app's endpoints, with expiration time of 10 seconds. If an expiration time isn't specified, it defaults to one minute.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

The following highlighted code creates two policies, each specifying a different expiration time. Selected endpoints can use the 20 second expiration, and others can use the 30 second expiration.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

You can select a policy for an endpoint when calling the CacheOutput method or using the [OutputCache] attribute:

app.MapGet("/20", Gravatar.WriteGravatar).CacheOutput("Expire20");
app.MapGet("/30", [OutputCache(PolicyName = "Expire30")] (context) => 
    Gravatar.WriteGravatar(context));

For apps with controllers, apply the [OutputCache] attribute to the action method. For Razor Pages apps, apply the attribute to the Razor page class.

Default output caching policy

By default, output caching follows these rules:

  • Only HTTP 200 responses are cached.
  • Only HTTP GET or HEAD requests are cached.
  • Responses that set cookies aren't cached.
  • Responses to authenticated requests aren't cached.

The following code applies all of the default caching rules to all of an app's endpoints:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Cache());
});

Override the default policy

The following code shows how to override the default rules. The highlighted lines in the following custom policy code enable caching for HTTP POST methods and HTTP 301 responses:

using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Primitives;

namespace OCMinimal;

public sealed class MyCustomPolicy : IOutputCachePolicy
{
    public static readonly MyCustomPolicy Instance = new();

    private MyCustomPolicy()
    {
    }

    ValueTask IOutputCachePolicy.CacheRequestAsync(
        OutputCacheContext context, 
        CancellationToken cancellationToken)
    {
        var attemptOutputCaching = AttemptOutputCaching(context);
        context.EnableOutputCaching = true;
        context.AllowCacheLookup = attemptOutputCaching;
        context.AllowCacheStorage = attemptOutputCaching;
        context.AllowLocking = true;

        // Vary by any query by default
        context.CacheVaryByRules.QueryKeys = "*";

        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeFromCacheAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeResponseAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        var response = context.HttpContext.Response;

        // Verify existence of cookie headers
        if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        // Check response code
        if (response.StatusCode != StatusCodes.Status200OK && 
            response.StatusCode != StatusCodes.Status301MovedPermanently)
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        return ValueTask.CompletedTask;
    }

    private static bool AttemptOutputCaching(OutputCacheContext context)
    {
        // Check if the current request fulfills the requirements
        // to be cached
        var request = context.HttpContext.Request;

        // Verify the method
        if (!HttpMethods.IsGet(request.Method) && 
            !HttpMethods.IsHead(request.Method) && 
            !HttpMethods.IsPost(request.Method))
        {
            return false;
        }

        // Verify existence of authorization headers
        if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || 
            request.HttpContext.User?.Identity?.IsAuthenticated == true)
        {
            return false;
        }

        return true;
    }
}

To use this custom policy, create a named policy:

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", MyCustomPolicy.Instance);
});

And select the named policy for an endpoint:

app.MapPost("/cachedpost", Gravatar.WriteGravatar)
    .CacheOutput("CachePost");

Alternative default policy override

Alternatively, use Dependency Injection (DI) to initialize an instance, with the following changes to the custom policy class:

  • A public constructor instead of a private constructor.
  • Eliminate the Instance property in the custom policy class.

For example:

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

The remainder of the class is the same as shown previously. Add the custom policy as shown in the following example:

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", builder => 
        builder.AddPolicy<MyCustomPolicy2>(), true);
});

The preceding code uses DI to create the instance of the custom policy class. Any public arguments in the constructor are resolved.

When using a custom policy as a base policy, don't call OutputCache() (with no arguments) on any endpoint that the base policy should apply to. Calling OutputCache() adds the default policy to the endpoint.

Specify the cache key

By default, every part of the URL is included as the key to a cache entry, that is, the scheme, host, port, path, and query string. However, you might want to explicitly control the cache key. For example, suppose you have an endpoint that returns a unique response only for each unique value of the culture query string. Variation in other parts of the URL, such as other query strings, shouldn't result in different cache entries. You can specify such rules in a policy, as shown in the following highlighted code:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

You can then select the VaryByQuery policy for an endpoint:

app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput("Query");

Here are some of the options for controlling the cache key:

  • SetVaryByQuery - Specify one or more query string names to add to the cache key.

  • SetVaryByHeader - Specify one or more HTTP headers to add to the cache key.

  • VaryByValue- Specify a value to add to the cache key. The following example uses a value that indicates whether the current server time in seconds is odd or even. A new response is generated only when the number of seconds goes from odd to even or even to odd.

    app.MapGet("/varybyvalue", Gravatar.WriteGravatar)
        .CacheOutput(c => c.VaryByValue((context) => 
            new KeyValuePair<string, string>(
                "time", (DateTime.Now.Second % 2)
                    .ToString(CultureInfo.InvariantCulture))));
    

Use OutputCacheOptions.UseCaseSensitivePaths to specify that the path part of the key is case sensitive. The default is case insensitive.

For more options, see the OutputCachePolicyBuilder class.

Cache revalidation

Cache revalidation means the server can return a 304 Not Modified HTTP status code instead of the full response body. This status code informs the client that the response to the request is unchanged from what the client previously received.

The following code illustrates the use of an Etag header to enable cache revalidation. If the client sends an If-None-Match header with the etag value of an earlier response, and the cache entry is fresh, the server returns 304 Not Modified instead of the full response:

app.MapGet("/etag", async (context) =>
{
    var etag = $"\"{Guid.NewGuid():n}\"";
    context.Response.Headers.ETag = etag;
    await Gravatar.WriteGravatar(context);

}).CacheOutput();

Another way to do cache revalidation is to check the date of the cache entry creation compared to the date requested by the client. When the request header If-Modified-Since is provided, output caching returns 304 if the cached entry is older and isn't expired.

Cache revalidation is automatic in response to these headers sent from the client. No special configuration is required on the server to enable this behavior, aside from enabling output caching.

Use tags to evict cache entries

You can use tags to identify a group of endpoints and evict all cache entries for the group. For example, the following code creates a pair of endpoints whose URLs begin with "blog", and tags them "tag-blog":

app.MapGet("/blog", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));
app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));

An alternative way to assign tags for the same pair of endpoints is to define a base policy that applies to endpoints that begin with blog:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Another alternative is to call MapGroup:

var blog = app.MapGroup("blog")
    .CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);

In the preceding tag assignment examples, both endpoints are identified by the tag-blog tag. You can then evict the cache entries for those endpoints with a single statement that references that tag:

app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) =>
{
    await cache.EvictByTagAsync(tag, default);
});

With this code, an HTTP POST request sent to https://localhost:<port>/purge/tag-blog will evict cache entries for these endpoints.

You might want a way to evict all cache entries for all endpoints. To do that, create a base policy for all endpoints as the following code does:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

This base policy enables you to use the "tag-all" tag to evict everything in cache.

Disable resource locking

By default, resource locking is enabled to mitigate the risk of cache stampede and thundering herd. For more information, see Output Caching.

To disable resource locking, call SetLocking(false) while creating a policy, as shown in the following example:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

The following example selects the no-locking policy for an endpoint:

app.MapGet("/nolock", Gravatar.WriteGravatar)
    .CacheOutput("NoLock");

Limits

The following properties of OutputCacheOptions let you configure limits that apply to all endpoints:

  • SizeLimit - Maximum size of cache storage. When this limit is reached, no new responses will be cached until older entries are evicted. Default value is 100 MB.
  • MaximumBodySize - If the response body exceeds this limit, it will not be cached. Default value is 64 MB.
  • DefaultExpirationTimeSpan - The expiration time duration that applies when not specified by a policy. Default value is 60 seconds.

Cache storage

IOutputCacheStore is used for storage. By default it's used with MemoryCache. We don't recommend IDistributedCache for use with output caching. IDistributedCache doesn't have atomic features, which are required for tagging. We recommend that you create custom IOutputCacheStore implementations by using direct dependencies on the underlying storage mechanism, such as Redis. Or use the built-in support for Redis cache in .NET 8..

See also