Intergiciel de mise en cache de sorite dans ASP.NET Core

Par Tom Dykstra

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

Cet article explique comment configurer l’intergiciel de mise en cache de sortie dans une application ASP.NET Core. Pour une présentation de la mise en cache de sortie, consultez Mise en cache de sortie.

L’intergiciel de mise en cache de sortie peut être utilisé dans tous les types d’applications ASP.NET Core : API minimale, API web avec contrôleurs, MVC et Razor Pages. L’exemple d’application est une API minimale, mais chaque fonctionnalité de mise en cache qu’elle illustre est également prise en charge dans les autres types d’application.

Ajouter l’intergiciel à l’application

Ajoutez l’intergiciel de mise en cache de sortie à la collection de services en appelant AddOutputCache.

Ajoutez l’intergiciel au pipeline de traitement des demandes en appelant UseOutputCache.

Notes

  • Dans les applications qui utilisent l’intergiciel CORS, UseOutputCache doit être appelé après UseCors.
  • Dans les applications Razor Pages et les applications avec contrôleurs, UseOutputCache doit être appelé après UseRouting.
  • Appeler AddOutputCache et UseOutputCache ne commence pas à mettre le comportement en cache, cela rend la mise en cache disponible. La mise en cache des données de réponse doit être configurée comme indiqué dans les sections suivantes.

Configurer un point de terminaison ou une page

Pour les applications API minimales, configurez un point de terminaison pour qu’il effectue la mise en cache en appelant CacheOutput ou en appliquant l’attribut [OutputCache] , comme illustré dans les exemples suivants :

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

Pour les applications avec des contrôleurs, appliquez l’attribut [OutputCache] pour la méthode d’action. Pour les applications Razor Pages, appliquez l’attribut à la classe de page Razor.

Configurer plusieurs points de terminaison ou pages

Créez des stratégies lors de l’appel de AddOutputCache pour spécifier la configuration de mise en cache qui s’applique à plusieurs points de terminaison. Une stratégie peut être sélectionnée pour des points de terminaison spécifiques, tandis qu’une stratégie de base fournit une configuration de mise en cache par défaut pour une collection de points de terminaison.

Le code suivant mis en évidence configure la mise en cache pour tous les points de terminaison de l’application, avec une durée d’expiration de 10 secondes. Si aucune heure d’expiration n’est spécifiée, elle est définie par défaut sur une 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)));
});

Le code suivant en surbrillance crée deux stratégies, chacune spécifiant une heure d’expiration différente. Les points de terminaison sélectionnés peuvent utiliser le délai d’expiration de 20 secondes, tandis que les autres peuvent utiliser le délai d’expiration de 30 secondes.

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

Vous pouvez sélectionner une stratégie pour un point de terminaison lors de l’appel de la méthode CacheOutput ou de l’attribut [OutputCache] :

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

Pour les applications avec des contrôleurs, appliquez l’attribut [OutputCache] pour la méthode d’action. Pour les applications Razor Pages, appliquez l’attribut à la classe de page Razor.

Stratégie de mise en cache de sortie par défaut

Par défaut, la mise en cache de sortie suit les règles suivantes :

  • Seules les réponses HTTP 200 sont mises en cache.
  • Seules les requêtes HTTP GET ou HEAD sont mises en cache.
  • Les réponses qui utilisent des cookie ne sont pas mises en cache.
  • Les réponses aux demandes authentifiées ne sont pas mises en cache.

Le code suivant applique toutes les règles de mise en cache par défaut à tous les points de terminaison d’une application :

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

Substituer la stratégie par défaut

Le code suivant montre comment substituer les règles par défaut. Les lignes mises en surbrillance dans le code de stratégie personnalisé suivant permettent la mise en cache pour les méthodes HTTP POST et les réponses 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;
    }
}

Pour utiliser cette stratégie personnalisée, créez une stratégie nommée :

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

Sélectionnez la stratégie nommée pour un point de terminaison :

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

Remplacement de stratégie par défaut alternative

Vous pouvez également utiliser l’injection de dépendances (DI) pour initialiser une instance, avec les modifications suivantes apportées à la classe de stratégie personnalisée :

  • Un constructeur public au lieu d’un constructeur privé.
  • Supprimez la propriété Instance dans la classe de stratégie personnalisée.

Par exemple :

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

Le reste de la classe est le même qu’indiqué précédemment. Ajoutez la stratégie personnalisée comme indiqué dans l’exemple suivant :

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

Le code précédent utilise l’ID pour créer l’instance de la classe de stratégie personnalisée. Tous les arguments publics dans le constructeur sont résolus.

Lorsque vous utilisez une stratégie personnalisée comme stratégie de base, n’appelez pas OutputCache() (sans argument) sur un point de terminaison auquel la stratégie de base doit s’appliquer. Appeler de OutputCache() ajoute la stratégie par défaut au point de terminaison.

Indiquer la clé cache

Par défaut, chaque partie de l’URL est incluse comme clé d’entrée de cache, c’est-à-dire le schéma, l’hôte, le port, le chemin d’accès et la chaîne de requête. Toutefois, vous pouvez contrôler explicitement la clé de cache. Par exemple, supposons que vous ayez un point de terminaison qui retourne une réponse unique seulement pour chaque valeur unique de la chaîne de requête culture. La variation d’autres parties de l’URL, telles que d’autres chaînes de requête, ne doit pas entraîner d’entrées de cache différentes. Vous pouvez spécifier ces règles dans une stratégie, comme indiqué dans le code en surbrillance suivant :

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

Vous pouvez ensuite sélectionner la stratégie VaryByQuery pour un point de terminaison :

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

Voici quelques-unes des options permettant de contrôler la clé de cache :

  • SetVaryByQuery – Spécifiez un ou plusieurs noms de chaîne de requête à ajouter à la clé de cache.

  • SetVaryByHeader – Spécifiez un ou plusieurs en-têtes HTTP à ajouter à la clé de cache.

  • VaryByValue – Spécifiez une valeur à ajouter à la clé de cache. L’exemple suivant utilise une valeur qui indique si l’heure actuelle du serveur en secondes est impaire ou égale. Une nouvelle réponse n’est générée que lorsque le nombre de secondes passe de impair à pair ou de pair à impair.

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

Utilisez OutputCacheOptions.UseCaseSensitivePaths pour spécifier que la partie chemin d’accès de la clé respecte la casse. La valeur web par défaut doit respecter la casse.

Pour plus d’options, consultez la classe OutputCachePolicyBuilder.

Revalidation du cache

La revalidation du cache signifie que le serveur peut retourner un code d’état HTTP 304 Not Modified au lieu d’un corps de réponse entier. Ce code statut informe le client que la réponse à la requête est inchangée par rapport à ce que le client a reçu précédemment.

Le code suivant illustre l’utilisation d’un en-tête Etag pour activer la revalidation du cache. Si le client envoie un en-tête If-None-Match avec la valeur etag d’une réponse antérieure et que l’entrée du cache est nouvelle, le serveur retourne 304 Non modifié au lieu de la réponse complète :

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

}).CacheOutput();

Une autre façon d’effectuer la revalidation du cache consiste à vérifier la date de création de l’entrée de cache par rapport à la date demandée par le client. Lorsque l’en-tête de demande If-Modified-Since est fourni, la mise en cache de sortie retourne 304 si l’entrée mise en cache est antérieure et n’a pas expiré.

La revalidation du cache est automatique en réponse à ces en-têtes envoyés à partir du client. Aucune configuration spéciale n’est requise sur le serveur pour activer ce comportement, à part l’activation de la mise en cache de sortie.

Utiliser des balises pour supprimer les entrées de cache

Vous pouvez utiliser des balises pour identifier un groupe de points de terminaison et supprimer toutes les entrées de cache pour le groupe. Par exemple, le code suivant crée une paire de points de terminaison dont les URL commencent par « blog » et les étiquettes « 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")); ;

Une autre façon d’affecter des balises pour la même paire de points de terminaison consiste à définir une stratégie de base qui s’applique aux points de terminaison qui commencent par 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));
});

Une autre solution consiste à appeler MapGroup :

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

Dans les exemples d’attribution de balise précédents, les deux points de terminaison sont identifiés par la balise tag-blog. Vous pouvez ensuite supprimer les entrées de cache pour ces points de terminaison avec une seule instruction qui fait référence à cette balise :

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

Avec ce code, une requête HTTP POST envoyée à https://localhost:<port>/purge/tag-blog supprime les entrées de cache pour ces points de terminaison.

Vous souhaiterez peut-être pouvoir effacer toutes les entrées de cache pour tous les points de terminaison. Pour ce faire, créez une stratégie de base pour tous les points de terminaison comme le fait le code suivant :

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

Cette stratégie de base vous permet d’utiliser la balise « tag-all » pour tout supprimer dans le cache.

Désactiver le verrouillage des ressources

Par défaut, le verrouillage des ressources est activé pour atténuer le risque de tamponnement du cache et de tonitruation du troupeau. Pour plus d’informations, consultez Cache de sortie.

Pour désactiver le verrouillage des ressources, appelez SetLocking(false) lors de la création d’une stratégie, comme illustré dans l’exemple suivant :

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

L’exemple suivant sélectionne la stratégie de non-verrouillage pour un point de terminaison :

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

Limites

Les propriétés suivantes de OutputCacheOptions vous permettent de configurer des limites qui s’appliquent à tous les points de terminaison :

  • SizeLimit – Taille maximale du stockage de cache. Quand cette limite est atteinte, aucune nouvelle réponse n’est mise en cache jusqu’à ce que les entrées plus anciennes soient supprimées. La valeur par défaut est 100 Mo.
  • MaximumBodySize – Si le corps de la réponse dépasse cette limite, elle n’est pas mise en cache. La valeur par défaut est 64 Mo.
  • DefaultExpirationTimeSpan – Durée d’expiration qui s’applique quand elle n’est pas spécifiée par une stratégie. La valeur par défaut est 60 secondes.

Stockage du cache

IOutputCacheStore est utilisé pour le stockage. Par défaut, il est utilisé avec MemoryCache. Les réponses mises en cache sont stockées In-process, de sorte que chaque serveur a un cache distinct qui est perdu chaque fois que le processus serveur est redémarré.

Cache Redis

Une alternative consiste à utiliser le cache Redis. Le cache Redis assure la cohérence entre les nœuds du serveur grâce à un cache partagé qui survit aux processus individuels du serveur. Pour utiliser Redis pour la mise en cache des sorties :

  • Installez le package NuGet Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.

  • Appelez builder.Services.AddStackExchangeRedisOutputCache (pas AddStackExchangeRedisCache) et spécifiez une chaîne de connexion qui pointe vers un serveur Redis.

    Par exemple :

    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 – Une chaîne de connexion à un serveur Redis local ou à une offre hébergée comme Azure Cache pour Redis. Par exemple, <instance_name>.redis.cache.windows.net:6380,password=<password>,ssl=True,abortConnect=False pour Azure Cache pour Redis.
    • options.InstanceName – Facultatif, spécifie une partition logique pour le cache.

    Les options de configuration sont identiques aux options de mise en cache distribuée basée sur Redis.

Nous vous déconseillons d’utiliser IDistributedCache avec la mise en cache de sortie. IDistributedCache n’a pas de caractéristiques atomiques, qui sont requises pour le balisage. Nous vous recommandons d’utiliser la prise en charge intégrée pour Redis ou de créer des implémentations de IOutputCacheStore personnalisées en utilisant des dépendances directes sur le mécanisme de stockage sous-jacent.

Voir aussi

Cet article explique comment configurer l’intergiciel de mise en cache de sortie dans une application ASP.NET Core. Pour une présentation de la mise en cache de sortie, consultez Mise en cache de sortie.

L’intergiciel de mise en cache de sortie peut être utilisé dans tous les types d’applications ASP.NET Core : API minimale, API web avec contrôleurs, MVC et Razor Pages. L’exemple d’application est une API minimale, mais chaque fonctionnalité de mise en cache qu’elle illustre est également prise en charge dans les autres types d’application.

Ajouter l’intergiciel à l’application

Ajoutez l’intergiciel de mise en cache de sortie à la collection de services en appelant AddOutputCache.

Ajoutez l’intergiciel au pipeline de traitement des demandes en appelant UseOutputCache.

Notes

  • Dans les applications qui utilisent l’intergiciel CORS, UseOutputCache doit être appelé après UseCors.
  • Dans les applications Razor Pages et les applications avec contrôleurs, UseOutputCache doit être appelé après UseRouting.
  • Appeler AddOutputCache et UseOutputCache ne commence pas à mettre le comportement en cache, cela rend la mise en cache disponible. La mise en cache des données de réponse doit être configurée comme indiqué dans les sections suivantes.

Configurer un point de terminaison ou une page

Pour les applications API minimales, configurez un point de terminaison pour qu’il effectue la mise en cache en appelant CacheOutput ou en appliquant l’attribut [OutputCache] , comme illustré dans les exemples suivants :

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

Pour les applications avec des contrôleurs, appliquez l’attribut [OutputCache] pour la méthode d’action. Pour les applications Razor Pages, appliquez l’attribut à la classe de page Razor.

Configurer plusieurs points de terminaison ou pages

Créez des stratégies lors de l’appel de AddOutputCache pour spécifier la configuration de mise en cache qui s’applique à plusieurs points de terminaison. Une stratégie peut être sélectionnée pour des points de terminaison spécifiques, tandis qu’une stratégie de base fournit une configuration de mise en cache par défaut pour une collection de points de terminaison.

Le code suivant mis en évidence configure la mise en cache pour tous les points de terminaison de l’application, avec une durée d’expiration de 10 secondes. Si aucune heure d’expiration n’est spécifiée, elle est définie par défaut sur une 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)));
});

Le code suivant en surbrillance crée deux stratégies, chacune spécifiant une heure d’expiration différente. Les points de terminaison sélectionnés peuvent utiliser l’expiration de 20 secondes, tandis que d’autres peuvent utiliser l’expiration de 30 secondes.

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

Vous pouvez sélectionner une stratégie pour un point de terminaison lors de l’appel de la méthode CacheOutput ou de l’attribut [OutputCache] :

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

Pour les applications avec des contrôleurs, appliquez l’attribut [OutputCache] pour la méthode d’action. Pour les applications Razor Pages, appliquez l’attribut à la classe de page Razor.

Stratégie de mise en cache de sortie par défaut

Par défaut, la mise en cache de sortie suit les règles suivantes :

  • Seules les réponses HTTP 200 sont mises en cache.
  • Seules les requêtes HTTP GET ou HEAD sont mises en cache.
  • Les réponses qui utilisent des cookie ne sont pas mises en cache.
  • Les réponses aux demandes authentifiées ne sont pas mises en cache.

Le code suivant applique toutes les règles de mise en cache par défaut à tous les points de terminaison d’une application :

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

Substituer la stratégie par défaut

Le code suivant montre comment substituer les règles par défaut. Les lignes mises en surbrillance dans le code de stratégie personnalisé suivant permettent la mise en cache pour les méthodes HTTP POST et les réponses 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;
    }
}

Pour utiliser cette stratégie personnalisée, créez une stratégie nommée :

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

Sélectionnez la stratégie nommée pour un point de terminaison :

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

Remplacement de stratégie par défaut alternative

Vous pouvez également utiliser l’injection de dépendances (DI) pour initialiser une instance, avec les modifications suivantes apportées à la classe de stratégie personnalisée :

  • Un constructeur public au lieu d’un constructeur privé.
  • Supprimez la propriété Instance dans la classe de stratégie personnalisée.

Par exemple :

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

Le reste de la classe est le même qu’indiqué précédemment. Ajoutez la stratégie personnalisée comme indiqué dans l’exemple suivant :

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

Le code précédent utilise l’ID pour créer l’instance de la classe de stratégie personnalisée. Tous les arguments publics dans le constructeur sont résolus.

Lorsque vous utilisez une stratégie personnalisée comme stratégie de base, n’appelez pas OutputCache() (sans argument) sur un point de terminaison auquel la stratégie de base doit s’appliquer. Appeler de OutputCache() ajoute la stratégie par défaut au point de terminaison.

Indiquer la clé cache

Par défaut, chaque partie de l’URL est incluse comme clé d’entrée de cache, c’est-à-dire le schéma, l’hôte, le port, le chemin d’accès et la chaîne de requête. Toutefois, vous pouvez contrôler explicitement la clé de cache. Par exemple, supposons que vous ayez un point de terminaison qui retourne une réponse unique seulement pour chaque valeur unique de la chaîne de requête culture. La variation d’autres parties de l’URL, telles que d’autres chaînes de requête, ne doit pas entraîner d’entrées de cache différentes. Vous pouvez spécifier ces règles dans une stratégie, comme indiqué dans le code en surbrillance suivant :

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

Vous pouvez ensuite sélectionner la stratégie VaryByQuery pour un point de terminaison :

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

Voici quelques-unes des options permettant de contrôler la clé de cache :

  • SetVaryByQuery – Spécifiez un ou plusieurs noms de chaîne de requête à ajouter à la clé de cache.

  • SetVaryByHeader – Spécifiez un ou plusieurs en-têtes HTTP à ajouter à la clé de cache.

  • VaryByValue – Spécifiez une valeur à ajouter à la clé de cache. L’exemple suivant utilise une valeur qui indique si l’heure actuelle du serveur en secondes est impaire ou égale. Une nouvelle réponse n’est générée que lorsque le nombre de secondes passe de impair à pair ou de pair à impair.

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

Utilisez OutputCacheOptions.UseCaseSensitivePaths pour spécifier que la partie chemin d’accès de la clé respecte la casse. La valeur web par défaut doit respecter la casse.

Pour plus d’options, consultez la classe OutputCachePolicyBuilder.

Revalidation du cache

La revalidation du cache signifie que le serveur peut retourner un code d’état HTTP 304 Not Modified au lieu d’un corps de réponse entier. Ce code statut informe le client que la réponse à la requête est inchangée par rapport à ce que le client a reçu précédemment.

Le code suivant illustre l’utilisation d’un en-tête Etag pour activer la revalidation du cache. Si le client envoie un en-tête If-None-Match avec la valeur etag d’une réponse antérieure et que l’entrée du cache est nouvelle, le serveur retourne 304 Non modifié au lieu de la réponse complète :

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

}).CacheOutput();

Une autre façon d’effectuer la revalidation du cache consiste à vérifier la date de création de l’entrée de cache par rapport à la date demandée par le client. Lorsque l’en-tête de demande If-Modified-Since est fourni, la mise en cache de sortie retourne 304 si l’entrée mise en cache est antérieure et n’a pas expiré.

La revalidation du cache est automatique en réponse à ces en-têtes envoyés à partir du client. Aucune configuration spéciale n’est requise sur le serveur pour activer ce comportement, à part l’activation de la mise en cache de sortie.

Utiliser des balises pour supprimer les entrées de cache

Vous pouvez utiliser des balises pour identifier un groupe de points de terminaison et supprimer toutes les entrées de cache pour le groupe. Par exemple, le code suivant crée une paire de points de terminaison dont les URL commencent par « blog » et les étiquettes « 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")); ;

Une autre façon d’affecter des balises pour la même paire de points de terminaison consiste à définir une stratégie de base qui s’applique aux points de terminaison qui commencent par 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));
});

Une autre solution consiste à appeler MapGroup :

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

Dans les exemples d’attribution de balise précédents, les deux points de terminaison sont identifiés par la balise tag-blog. Vous pouvez ensuite supprimer les entrées de cache pour ces points de terminaison avec une seule instruction qui fait référence à cette balise :

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

Avec ce code, une requête HTTP POST envoyée à https://localhost:<port>/purge/tag-blog supprime les entrées de cache pour ces points de terminaison.

Vous souhaiterez peut-être pouvoir effacer toutes les entrées de cache pour tous les points de terminaison. Pour ce faire, créez une stratégie de base pour tous les points de terminaison comme le fait le code suivant :

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

Cette stratégie de base vous permet d’utiliser la balise « tag-all » pour tout supprimer dans le cache.

Désactiver le verrouillage des ressources

Par défaut, le verrouillage des ressources est activé pour atténuer le risque de tamponnement du cache et de tonitruation du troupeau. Pour plus d’informations, consultez Cache de sortie.

Pour désactiver le verrouillage des ressources, appelez SetLocking(false) lors de la création d’une stratégie, comme illustré dans l’exemple suivant :

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

L’exemple suivant sélectionne la stratégie de non-verrouillage pour un point de terminaison :

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

Limites

Les propriétés suivantes de OutputCacheOptions vous permettent de configurer des limites qui s’appliquent à tous les points de terminaison :

  • SizeLimit – Taille maximale du stockage de cache. Lorsque cette limite est atteinte, aucune nouvelle réponse n’est mise en cache tant que les entrées plus anciennes n’ont pas été supprimées. La valeur par défaut est 100 Mo.
  • MaximumBodySize – Si le corps de la réponse dépasse cette limite, elle ne sera pas mise en cache. La valeur par défaut est 64 Mo.
  • DefaultExpirationTimeSpan – Durée d’expiration qui s’applique quand elle n’est pas spécifiée par une stratégie. La valeur par défaut est 60 secondes.

Stockage du cache

IOutputCacheStore est utilisé pour le stockage. Par défaut, il est utilisé avec MemoryCache. Nous vous déconseillons d’utiliser IDistributedCache avec la mise en cache de sortie. IDistributedCache n’a pas de caractéristiques atomiques, qui sont requises pour le balisage. Nous vous recommandons de créer des implémentations IOutputCacheStore personnalisées à l’aide de dépendances directes sur le mécanisme de stockage sous-jacent, tel que Redis. Vous pouvez aussi utiliser la prise en charge intégrée pour le cache Redis dans .NET 8.

Voir aussi