Middleware de almacenamiento en caché de salida en ASP.NET Core

Por Tom Dykstra

Nota:

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

En este artículo se explica cómo configurar el middleware de almacenamiento en caché de salida en una aplicación ASP.NET Core. Para obtener una introducción al almacenamiento en caché de salida, consulte Almacenamiento en caché de salida.

El middleware de almacenamiento en caché de salida se puede usar en todos los tipos de aplicaciones de ASP.NET Core: API mínima, API web con controladores, MVC y Razor Pages. La aplicación de ejemplo es una API mínima, pero todas las características de almacenamiento en caché que ilustra también se admiten en los otros tipos de aplicación.

Adición del middleware a la aplicación

Agregue el middleware de almacenamiento en caché de salida a la colección de servicios mediante una llamada a AddOutputCache.

Agregue el middleware a la canalización de procesamiento de solicitudes mediante una llamada a UseOutputCache.

Nota:

  • En las aplicaciones que usan middleware de CORS, UseOutputCache se debe llamar a después de UseCors.
  • En las aplicaciones de Razor Pages y aplicaciones con controladores, UseOutputCache se debe llamar a después de UseRouting.
  • Llamar a AddOutputCache y UseOutputCache no inicia el comportamiento del almacenamiento en caché, hace que el almacenamiento en caché esté disponible. Los datos de respuesta de almacenamiento en caché deben configurarse como se muestra en las secciones siguientes.

Configuración de un punto de conexión o página

Para las aplicaciones de API mínimas, configure un punto de conexión para realizar el almacenamiento en caché mediante una llamada a CacheOutput o aplicando el atributo [OutputCache], como se muestra en los ejemplos siguientes:

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

En el caso de las aplicaciones con controladores, aplique el atributo [OutputCache] al método de acción. En el caso de las aplicaciones de Razor Pages, aplique el atributo a la clase de página Razor.

Configuración de varios puntos de conexión o páginas

Cree directivas al llamar a AddOutputCache para especificar la configuración de almacenamiento en caché que se aplica a varios puntos de conexión. Se puede seleccionar una directiva para puntos de conexión específicos, mientras que una directiva base proporciona una configuración de almacenamiento en caché predeterminada para una colección de puntos de conexión.

El código resaltado siguiente configura el almacenamiento en caché para todos los puntos de conexión de la aplicación, con un tiempo de expiración de 10 segundos. Si no se especifica una hora de expiración, el valor predeterminado es de un minuto.

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)));
});

El código resaltado siguiente crea dos directivas, cada una especificando una hora de expiración diferente. Los puntos de conexión seleccionados pueden usar la expiración de 20 segundos y otros pueden usar la expiración de 30 segundos.

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)));
});

Puede seleccionar una directiva para un punto de conexión al llamar al método CacheOutput o mediante el atributo [OutputCache]:

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

En el caso de las aplicaciones con controladores, aplique el atributo [OutputCache] al método de acción. En el caso de las aplicaciones de Razor Pages, aplique el atributo a la clase de página Razor.

Directiva de almacenamiento en caché de salida predeterminada

De forma predeterminada, el almacenamiento en caché de salida sigue estas reglas:

  • Solo se almacenan en caché las respuestas HTTP 200.
  • Solo se almacenan en caché las solicitudes HTTP GET o HEAD.
  • Las respuestas que establecen cookie no se almacenan en caché.
  • Las respuestas a las solicitudes autenticadas no se almacenan en caché.

El código siguiente aplica todas las reglas de almacenamiento en caché predeterminadas a todos los puntos de conexión de una aplicación:

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

Invalidación de la directiva predeterminada

En el código siguiente se muestra cómo invalidar las reglas predeterminadas. Las líneas resaltadas en el siguiente código de directiva personalizado habilitan el almacenamiento en caché para los métodos HTTP POST y las respuestas HTTP 301:

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;
    }
}

Para usar esta directiva personalizada, cree una directiva con nombre:

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

Y seleccione la directiva con nombre para un punto de conexión:

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

Invalidación de directiva predeterminada alternativa

Como alternativa, use la inserción de dependencias (DI) para inicializar una instancia, con los siguientes cambios en la clase de directiva personalizada:

  • Un constructor público en lugar de un constructor privado.
  • Elimine la propiedad Instance de la clase de directiva personalizada.

Por ejemplo:

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

El resto de la clase es el mismo que se mostró anteriormente. Agregue la directiva personalizada como se muestra en el ejemplo siguiente:

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

El código anterior usa DI para crear la instancia de la clase de directiva personalizada. Se resuelven todos los argumentos públicos del constructor.

Al usar una directiva personalizada como directiva base, no llame a OutputCache() (sin argumentos) en ningún punto de conexión al que se deba aplicar la directiva base. La llamada a OutputCache() agrega la directiva predeterminada al punto de conexión.

Especificación de la clave de caché

De forma predeterminada, cada parte de la dirección URL se incluye como clave para una entrada de caché, es decir, el esquema, el host, el puerto, la ruta de acceso y la cadena de consulta. Sin embargo, es posible que desee controlar explícitamente la clave de caché. Por ejemplo, supongamos que tiene un punto de conexión que devuelve una respuesta única solo para cada valor único de la cadena de consulta de culture. La variación en otras partes de la dirección URL, como otras cadenas de consulta, no debería dar lugar a entradas de caché diferentes. Puede especificar estas reglas en una directiva, como se muestra en el código resaltado siguiente:

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));
});

Después, puede seleccionar la directiva VaryByQuery para un punto de conexión:

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

Estas son algunas de las opciones para controlar la clave de caché:

  • SetVaryByQuery : especifique uno o varios nombres de cadena de consulta para agregar a la clave de caché.

  • SetVaryByHeader : especifique uno o varios encabezados HTTP para agregar a la clave de caché.

  • VaryByValue: especifique un valor que se va a agregar a la clave de caché. En el ejemplo siguiente se usa un valor que indica si la hora actual del servidor en segundos es par o impar. Solo se genera una nueva respuesta cuando el número de segundos va de impar a par o de par a impar.

    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 para especificar que la parte de ruta de acceso de la clave distingue mayúsculas de minúsculas. El valor predeterminado no distingue mayúsculas de minúsculas.

Para encontrar más opciones, consulte la clase OutputCachePolicyBuilder.

Revalidación de caché

La revalidación de caché significa que el servidor puede devolver un código de estado HTTP 304 Not Modified en lugar de todo el cuerpo de respuesta. Este código de estado informa al cliente de que la respuesta a la solicitud no cambia de lo que el cliente recibió anteriormente.

En el código siguiente se muestra el uso de un encabezado Etag para habilitar la revalidación de caché. Si el cliente envía un encabezado If-None-Match con el valor ETag de una respuesta anterior y la entrada de caché es nueva, el servidor devuelve 304 No modificado en lugar de la respuesta completa:

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

}).CacheOutput();

Otra manera de revalidar la caché es comprobar la fecha de creación de la entrada de caché en comparación con la fecha solicitada por el cliente. Cuando se proporciona el encabezado de solicitud If-Modified-Since, el almacenamiento en caché de salida devuelve 304 si la entrada almacenada en caché es anterior y no ha expirado.

La revalidación de caché es automática en respuesta a estos encabezados enviados desde el cliente. No se requiere ninguna configuración especial en el servidor para habilitar este comportamiento, aparte de habilitar el almacenamiento en caché de salida.

Uso de etiquetas para expulsar entradas de caché

Puede usar etiquetas para identificar un grupo de puntos de conexión y expulsar todas las entradas de caché del grupo. Por ejemplo, el código siguiente crea un par de puntos de conexión cuyas direcciones URL comienzan por "blog" y las etiqueta "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")); ;

Una manera alternativa de asignar etiquetas para el mismo par de puntos de conexión es definir una directiva base que se aplica a los puntos de conexión que comienzan por 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));
});

Otra alternativa es llamar a MapGroup:

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

En los ejemplos de asignación de etiquetas anteriores, ambos puntos de conexión se identifican mediante la etiqueta tag-blog. Después, puede expulsar las entradas de caché de esos puntos de conexión con una sola instrucción que haga referencia a esa etiqueta:

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

Con este código, una solicitud HTTP POST enviada a https://localhost:<port>/purge/tag-blog expulsa las entradas de caché para estos puntos de conexión.

Es posible que desee una manera de expulsar todas las entradas de caché para todos los puntos de conexión. Para ello, cree una directiva base para todos los puntos de conexión, como hace el código siguiente:

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));
});

Esta directiva base permite usar la etiqueta "tag-all" para expulsar todo de la memoria caché.

Deshabilitación del bloqueo de recursos

De forma predeterminada, el bloqueo de recursos se habilita para mitigar el riesgo de fallo en cascada y colapso por activación simultánea de varios hilos de ejecución (thundering herd). Para más información, vea Almacenamiento en caché de salida.

Para deshabilitar el bloqueo de recursos, llame a SetLocking(false) al crear una directiva, como se muestra en el ejemplo siguiente:

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));
});

En el ejemplo siguiente se selecciona la directiva sin bloqueo para un punto de conexión:

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

Límites

Las siguientes propiedades de OutputCacheOptions permiten configurar límites que se aplican a todos los puntos de conexión:

  • SizeLimit - Tamaño máximo del almacenamiento en caché. Cuando se alcanza este límite, no se almacenan en caché nuevas respuestas hasta que se expulsen las entradas anteriores. El valor predeterminado es 100 MB.
  • MaximumBodySize - Si el cuerpo de la respuesta supera este límite, no se almacena en caché. El valor predeterminado es 64 MB.
  • DefaultExpirationTimeSpan - La duración del tiempo de expiración que se aplica cuando no se especifica en una directiva. El valor predeterminado es de 60 segundos.

Almacenamiento en caché

IOutputCacheStore se usa para el almacenamiento. De forma predeterminada, se usa con MemoryCache. Las respuestas almacenadas en caché se almacenan en proceso, por lo que cada servidor tiene una caché independiente que se pierde cada vez que se reinicia el proceso del servidor.

Redis Cache

Una alternativa es usar caché de Redis . La caché de Redis proporciona coherencia entre los nodos de servidor a través de una caché compartida que sobrevive a procesos de servidor individuales. Para usar Redis para el almacenamiento en caché de salida:

  • Instale el paquete NuGet Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.

  • Llame a builder.Services.AddStackExchangeRedisOutputCache (no a AddStackExchangeRedisCache) y proporcione un cadena de conexión que apunte a un servidor de Redis.

    Por ejemplo:

    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: una cadena de conexión a un servidor de Redis local o a una oferta hospedada, como Azure Cache for Redis. Por ejemplo, <instance_name>.redis.cache.windows.net:6380,password=<password>,ssl=True,abortConnect=False para Azure Cache for Redis.
    • options.InstanceName: opcional, especifica una partición lógica para la memoria caché.

    Las opciones de configuración son idénticas a las opciones de almacenamiento en caché distribuida basadas en Redis.

No se recomienda usar IDistributedCache con el almacenamiento en caché de salida. IDistributedCache no tiene características atómicas, que son necesarias para el etiquetado. Se recomienda usar la compatibilidad integrada con Redis o crear implementaciones personalizadas IOutputCacheStore mediante dependencias directas en el mecanismo de almacenamiento subyacente.

Consulte también

En este artículo se explica cómo configurar el middleware de almacenamiento en caché de salida en una aplicación ASP.NET Core. Para obtener una introducción al almacenamiento en caché de salida, consulte Almacenamiento en caché de salida.

El middleware de almacenamiento en caché de salida se puede usar en todos los tipos de aplicaciones de ASP.NET Core: API mínima, API web con controladores, MVC y Razor Pages. La aplicación de ejemplo es una API mínima, pero todas las características de almacenamiento en caché que ilustra también se admiten en los otros tipos de aplicación.

Adición del middleware a la aplicación

Agregue el middleware de almacenamiento en caché de salida a la colección de servicios mediante una llamada a AddOutputCache.

Agregue el middleware a la canalización de procesamiento de solicitudes mediante una llamada a UseOutputCache.

Nota:

  • En las aplicaciones que usan middleware de CORS, UseOutputCache se debe llamar a después de UseCors.
  • En las aplicaciones de Razor Pages y aplicaciones con controladores, UseOutputCache se debe llamar a después de UseRouting.
  • Llamar a AddOutputCache y UseOutputCache no inicia el comportamiento del almacenamiento en caché, hace que el almacenamiento en caché esté disponible. Los datos de respuesta de almacenamiento en caché deben configurarse como se muestra en las secciones siguientes.

Configuración de un punto de conexión o página

Para las aplicaciones de API mínimas, configure un punto de conexión para realizar el almacenamiento en caché mediante una llamada a CacheOutput o aplicando el atributo [OutputCache], como se muestra en los ejemplos siguientes:

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

En el caso de las aplicaciones con controladores, aplique el atributo [OutputCache] al método de acción. En el caso de las aplicaciones de Razor Pages, aplique el atributo a la clase de página Razor.

Configuración de varios puntos de conexión o páginas

Cree directivas al llamar a AddOutputCache para especificar la configuración de almacenamiento en caché que se aplica a varios puntos de conexión. Se puede seleccionar una directiva para puntos de conexión específicos, mientras que una directiva base proporciona una configuración de almacenamiento en caché predeterminada para una colección de puntos de conexión.

El código resaltado siguiente configura el almacenamiento en caché para todos los puntos de conexión de la aplicación, con un tiempo de expiración de 10 segundos. Si no se especifica una hora de expiración, el valor predeterminado es de un minuto.

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)));
});

El código resaltado siguiente crea dos directivas, cada una especificando una hora de expiración diferente. Los puntos de conexión seleccionados pueden usar la expiración de 20 segundos y otros pueden usar la expiración de 30 segundos.

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)));
});

Puede seleccionar una directiva para un punto de conexión al llamar al método CacheOutput o mediante el atributo [OutputCache]:

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

En el caso de las aplicaciones con controladores, aplique el atributo [OutputCache] al método de acción. En el caso de las aplicaciones de Razor Pages, aplique el atributo a la clase de página Razor.

Directiva de almacenamiento en caché de salida predeterminada

De forma predeterminada, el almacenamiento en caché de salida sigue estas reglas:

  • Solo se almacenan en caché las respuestas HTTP 200.
  • Solo se almacenan en caché las solicitudes HTTP GET o HEAD.
  • Las respuestas que establecen cookie no se almacenan en caché.
  • Las respuestas a las solicitudes autenticadas no se almacenan en caché.

El código siguiente aplica todas las reglas de almacenamiento en caché predeterminadas a todos los puntos de conexión de una aplicación:

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

Invalidación de la directiva predeterminada

En el código siguiente se muestra cómo invalidar las reglas predeterminadas. Las líneas resaltadas en el siguiente código de directiva personalizado habilitan el almacenamiento en caché para los métodos HTTP POST y las respuestas HTTP 301:

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;
    }
}

Para usar esta directiva personalizada, cree una directiva con nombre:

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

Y seleccione la directiva con nombre para un punto de conexión:

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

Invalidación de directiva predeterminada alternativa

Como alternativa, use la inserción de dependencias (DI) para inicializar una instancia, con los siguientes cambios en la clase de directiva personalizada:

  • Un constructor público en lugar de un constructor privado.
  • Elimine la propiedad Instance de la clase de directiva personalizada.

Por ejemplo:

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

El resto de la clase es el mismo que se mostró anteriormente. Agregue la directiva personalizada como se muestra en el ejemplo siguiente:

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

El código anterior usa DI para crear la instancia de la clase de directiva personalizada. Se resuelven todos los argumentos públicos del constructor.

Al usar una directiva personalizada como directiva base, no llame a OutputCache() (sin argumentos) en ningún punto de conexión al que se deba aplicar la directiva base. La llamada a OutputCache() agrega la directiva predeterminada al punto de conexión.

Especificación de la clave de caché

De forma predeterminada, cada parte de la dirección URL se incluye como clave para una entrada de caché, es decir, el esquema, el host, el puerto, la ruta de acceso y la cadena de consulta. Sin embargo, es posible que desee controlar explícitamente la clave de caché. Por ejemplo, supongamos que tiene un punto de conexión que devuelve una respuesta única solo para cada valor único de la cadena de consulta de culture. La variación en otras partes de la dirección URL, como otras cadenas de consulta, no debería dar lugar a entradas de caché diferentes. Puede especificar estas reglas en una directiva, como se muestra en el código resaltado siguiente:

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));
});

Después, puede seleccionar la directiva VaryByQuery para un punto de conexión:

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

Estas son algunas de las opciones para controlar la clave de caché:

  • SetVaryByQuery : especifique uno o varios nombres de cadena de consulta para agregar a la clave de caché.

  • SetVaryByHeader : especifique uno o varios encabezados HTTP para agregar a la clave de caché.

  • VaryByValue: especifique un valor que se va a agregar a la clave de caché. En el ejemplo siguiente se usa un valor que indica si la hora actual del servidor en segundos es par o impar. Solo se genera una nueva respuesta cuando el número de segundos va de impar a par o de par a impar.

    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 para especificar que la parte de ruta de acceso de la clave distingue mayúsculas de minúsculas. El valor predeterminado no distingue mayúsculas de minúsculas.

Para encontrar más opciones, consulte la clase OutputCachePolicyBuilder.

Revalidación de caché

La revalidación de caché significa que el servidor puede devolver un código de estado HTTP 304 Not Modified en lugar de todo el cuerpo de respuesta. Este código de estado informa al cliente de que la respuesta a la solicitud no cambia de lo que el cliente recibió anteriormente.

En el código siguiente se muestra el uso de un encabezado Etag para habilitar la revalidación de caché. Si el cliente envía un encabezado If-None-Match con el valor ETag de una respuesta anterior y la entrada de caché es nueva, el servidor devuelve 304 No modificado en lugar de la respuesta completa:

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

}).CacheOutput();

Otra manera de revalidar la caché es comprobar la fecha de creación de la entrada de caché en comparación con la fecha solicitada por el cliente. Cuando se proporciona el encabezado de solicitud If-Modified-Since, el almacenamiento en caché de salida devuelve 304 si la entrada almacenada en caché es anterior y no ha expirado.

La revalidación de caché es automática en respuesta a estos encabezados enviados desde el cliente. No se requiere ninguna configuración especial en el servidor para habilitar este comportamiento, aparte de habilitar el almacenamiento en caché de salida.

Uso de etiquetas para expulsar entradas de caché

Puede usar etiquetas para identificar un grupo de puntos de conexión y expulsar todas las entradas de caché del grupo. Por ejemplo, el código siguiente crea un par de puntos de conexión cuyas direcciones URL comienzan por "blog" y las etiqueta "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")); ;

Una manera alternativa de asignar etiquetas para el mismo par de puntos de conexión es definir una directiva base que se aplica a los puntos de conexión que comienzan por 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));
});

Otra alternativa es llamar a MapGroup:

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

En los ejemplos de asignación de etiquetas anteriores, ambos puntos de conexión se identifican mediante la etiqueta tag-blog. Después, puede expulsar las entradas de caché de esos puntos de conexión con una sola instrucción que haga referencia a esa etiqueta:

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

Con este código, una solicitud HTTP POST enviada a https://localhost:<port>/purge/tag-blog expulsará las entradas de caché para estos puntos de conexión.

Es posible que desee una manera de expulsar todas las entradas de caché para todos los puntos de conexión. Para ello, cree una directiva base para todos los puntos de conexión, como hace el código siguiente:

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));
});

Esta directiva base permite usar la etiqueta "tag-all" para expulsar todo de la memoria caché.

Deshabilitación del bloqueo de recursos

De forma predeterminada, el bloqueo de recursos se habilita para mitigar el riesgo de fallo en cascada y colapso por activación simultánea de varios hilos de ejecución (thundering herd). Para más información, vea Almacenamiento en caché de salida.

Para deshabilitar el bloqueo de recursos, llame a SetLocking(false) al crear una directiva, como se muestra en el ejemplo siguiente:

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));
});

En el ejemplo siguiente se selecciona la directiva sin bloqueo para un punto de conexión:

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

Límites

Las siguientes propiedades de OutputCacheOptions permiten configurar límites que se aplican a todos los puntos de conexión:

  • SizeLimit - Tamaño máximo del almacenamiento en caché. Cuando se alcanza este límite, no se almacenarán en caché nuevas respuestas hasta que se expulsen las entradas anteriores. El valor predeterminado es 100 MB.
  • MaximumBodySize - Si el cuerpo de la respuesta supera este límite, no se almacenará en caché. El valor predeterminado es 64 MB.
  • DefaultExpirationTimeSpan - La duración del tiempo de expiración que se aplica cuando no se especifica en una directiva. El valor predeterminado es de 60 segundos.

Almacenamiento en caché

IOutputCacheStore se usa para el almacenamiento. De forma predeterminada, se usa con MemoryCache. No se recomienda usar IDistributedCache con el almacenamiento en caché de salida. IDistributedCache no tiene características atómicas, que son necesarias para el etiquetado. Se recomienda crear implementaciones IOutputCacheStore personalizadas mediante dependencias directas en el mecanismo de almacenamiento subyacente, como Redis. O bien, usar la compatibilidad integrada con la caché de Redis en .NET 8..

Consulte también