Mettre en cache en mémoire dans ASP.NET Core

Par Rick Anderson, John Luo et Steve Smith

La mise en cache peut améliorer considérablement les performances et la scalabilité d’une application en réduisant le travail nécessaire pour générer du contenu. La mise en cache fonctionne mieux avec les données qui changent rarement et sont coûteuses à générer. La mise en cache crée une copie des données qui peut être retournée beaucoup plus rapidement qu’à partir de la source. Les applications doivent être écrites et testées pour ne jamais dépendre des données mises en cache.

ASP.NET Core prend en charge plusieurs caches différents. Le cache le plus simple est basé sur IMemoryCache. IMemoryCache représente un cache stocké dans la mémoire du serveur web. Les applications exécutées sur une batterie de serveurs (plusieurs serveurs) doivent s’assurer que les sessions sont persistantes lorsqu’elles utilisent du cache en mémoire. Les sessions persistantes garantissent que les requêtes d’un client sont toutes envoyées au même serveur. Par exemple, les applications Azure Web utilisent Application Request Routing (ARR) pour acheminer toutes les requêtes vers le même serveur.

Les sessions non persistantes dans une batterie de serveurs web nécessitent un cache distribué pour éviter les problèmes de cohérence du cache. Pour certaines applications, un cache distribué peut prendre en charge un scale-out plus élevé qu’un cache en mémoire. L’utilisation d’un cache distribué décharge la mémoire cache vers un processus externe.

Le cache en mémoire peut stocker n’importe quel objet. L’interface de cache distribué est limitée au byte[]. Le cache en mémoire et le cache distribué stockent les éléments de cache sous forme de paires clé-valeur.

System.Runtime.Caching/MemoryCache

System.Runtime.Caching/MemoryCache (package NuGet) peut être utilisé avec :

  • .NET Standard 2.0 ou version ultérieure.
  • Une implémentation .NET qui cible .NET Standard 2.0 ou version ultérieure. Par exemple, ASP.NET Core 3.1 ou version ultérieure.
  • .NET Framework 4.5 ou ultérieur.

Microsoft.Extensions.Caching.Memory/IMemoryCache (décrit dans cet article) est recommandé, plutôt que System.Runtime.Caching/MemoryCache, car il est mieux intégré à ASP.NET Core. Par exemple, IMemoryCache fonctionne en mode natif avec l’injection de dépendances ASP.NET Core.

Utilisez System.Runtime.Caching/MemoryCache comme pont de compatibilité lors du portage du code d’ASP.NET 4.x vers ASP.NET Core.

Recommandations sur le cache

  • Le code doit toujours avoir une option de secours pour extraire des données et ne pas dépendre de la disponibilité d’une valeur mise en cache.
  • Le cache utilise une ressource rare, la mémoire. Limitez la croissance du cache :
    • N’insérez pas d’entrée externe dans le cache. Par exemple, l’utilisation d’une entrée arbitraire fournie par l’utilisateur comme clé de cache n’est pas recommandée, car l’entrée peut consommer une quantité imprévisible de mémoire.
    • Utilisez des expirations pour limiter la croissance du cache.
    • Utilisez SetSize, Size et SizeLimit pour limiter la taille du cache. Le runtime ASP.NET Core ne limite pas la taille du cache en fonction de la sollicitation de la mémoire. Il appartient au développeur de limiter la taille du cache.

Utiliser IMemoryCache

Avertissement

L’utilisation d’un cache de mémoire partagé à partir de l’injection de dépendances et l’appel de SetSize, Sizeou SizeLimit pour limiter la taille du cache peuvent entraîner l’échec de l’application. Lorsqu’une limite de taille est définie sur un cache, toutes les entrées doivent spécifier une taille lors de l’ajout. Cela peut entraîner des problèmes, car les développeurs peuvent ne pas avoir un contrôle total sur ce qui utilise le cache partagé. Lorsque vous utilisez SetSize, Size ou SizeLimit pour limiter le cache, créez un singleton de cache pour la mise en cache. Pour plus d’informations et un exemple, consultez Utiliser SetSize, Size et SizeLimit pour limiter la taille du cache. Un cache partagé est partagé par d’autres frameworks ou bibliothèques.

La mise en cache en mémoire est un service référencé à partir d’une application à l’aide de l’injection de dépendances. Interrogez l’instance IMemoryCache dans le constructeur :

public class IndexModel : PageModel
{
    private readonly IMemoryCache _memoryCache;

    public IndexModel(IMemoryCache memoryCache) =>
        _memoryCache = memoryCache;

    // ...

Le code suivant utilise TryGetValue pour vérifier si une heure est dans le cache. Si aucune heure n’est mise en cache, une nouvelle entrée est créée et ajoutée au cache avec Set :

public void OnGet()
{
    CurrentDateTime = DateTime.Now;

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = CurrentDateTime;

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }

    CacheCurrentDateTime = cacheValue;
}

Dans le code précédent, l’entrée du cache est configurée avec une expiration glissante de trois secondes. Si aucun accès à l’entrée de cache n’est effectué pendant plus de trois secondes, elle est supprimée du cache. Chaque fois qu’un accès à l’entrée du cache est effectué, elle reste dans le cache pendant 3 secondes supplémentaires. La classe CacheKeys fait partie de l’exemple en téléchargement.

L’heure actuelle et l’heure mise en cache sont affichées :

<ul>
    <li>Current Time: @Model.CurrentDateTime</li>
    <li>Cached Time: @Model.CacheCurrentDateTime</li>
</ul>

Le code suivant utilise la méthode d’extension Set pour mettre en cache les données pendant une durée relative sans MemoryCacheEntryOptions :

_memoryCache.Set(CacheKeys.Entry, DateTime.Now, TimeSpan.FromDays(1));

Dans le code précédent, l’entrée de cache est configurée avec une expiration relative d’un jour. L’entrée de cache est supprimée du cache après un jour, même si aucun accès n’est effectué dans ce délai d’expiration.

Le code suivant utilise GetOrCreate et GetOrCreateAsync pour mettre les données en cache.

public void OnGetCacheGetOrCreate()
{
    var cachedValue = _memoryCache.GetOrCreate(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return DateTime.Now;
        });

    // ...
}

public async Task OnGetCacheGetOrCreateAsync()
{
    var cachedValue = await _memoryCache.GetOrCreateAsync(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    // ...
}

Le code suivant appelle Get pour récupérer l’heure mise en cache :

var cacheEntry = _memoryCache.Get<DateTime?>(CacheKeys.Entry);

Le code suivant récupère ou crée un élément mis en cache avec une expiration absolue :

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.Entry,
    cacheEntry =>
    {
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

Un éléments mis en cache défini uniquement avec une expiration glissante risque de ne jamais expirer. Si l’élément mis en cache est accédé à plusieurs reprises dans l’intervalle d’expiration glissant, l’élément n’expire jamais. Combinez une expiration glissante avec une expiration absolue pour garantir l’expiration de l’élément. L’expiration absolue définit une limite supérieure sur la durée pendant laquelle l’élément peut être mis en cache tout en permettant à l’élément d’expirer plus tôt s’il n’est pas interrogé dans l’intervalle d’expiration glissant. Si l’intervalle d’expiration glissant ou le délai d’expiration absolu passent, l’élément est supprimé du cache.

Le code suivant récupère ou crée un élément mis en cache avec une expiration glissante et une expiration absolue :

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.CallbackEntry,
    cacheEntry =>
    {
        cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

Le code précédent garantit que les données ne seront pas mises en cache plus longtemps que le délai d’expiration absolu.

GetOrCreate, GetOrCreateAsync et Get sont des méthodes d’extension dans la classe CacheExtensions. Ces méthodes étendent la fonctionnalité de IMemoryCache.

MemoryCacheEntryOptions

L’exemple suivant :

  • Définit la priorité du cache sur CacheItemPriority.NeverRemove.
  • Définit un PostEvictionDelegate qui est appelé après la suppression de l’entrée du cache. Le rappel est exécuté sur un thread différent du code qui supprime l’élément du cache.
public void OnGetCacheRegisterPostEvictionCallback()
{
    var memoryCacheEntryOptions = new MemoryCacheEntryOptions()
        .SetPriority(CacheItemPriority.NeverRemove)
        .RegisterPostEvictionCallback(PostEvictionCallback, _memoryCache);

    _memoryCache.Set(CacheKeys.CallbackEntry, DateTime.Now, memoryCacheEntryOptions);
}

private static void PostEvictionCallback(
    object cacheKey, object cacheValue, EvictionReason evictionReason, object state)
{
    var memoryCache = (IMemoryCache)state;

    memoryCache.Set(
        CacheKeys.CallbackMessage,
        $"Entry {cacheKey} was evicted: {evictionReason}.");
}

Utiliser SetSize, Size et SizeLimit pour limiter la taille du cache

Une instance MemoryCache peut éventuellement spécifier et appliquer une limite de taille. La limite de taille du cache n’a pas d’unité de mesure définie, car le cache n’a aucun mécanisme pour mesurer la taille des entrées. Si la limite de taille du cache est définie, toutes les entrées doivent spécifier la taille. Le runtime ASP.NET Core ne limite pas la taille du cache en fonction de la sollicitation de la mémoire. Il appartient au développeur de limiter la taille du cache. La taille spécifiée est en unités choisies par le développeur.

Par exemple :

  • Si l’application web a principalement mis en cache des chaînes, chaque taille d’entrée de cache peut être la longueur de la chaîne.
  • L’application peut spécifier la taille de toutes les entrées à 1, et la limite de taille correspond au nombre d’entrées.

Si SizeLimit n’est pas défini, la taille du cache augmente sans limite. Le runtime ASP.NET Core ne découpe pas le cache lorsque la mémoire système est faible. Les applications doivent être conçues pour :

  • Limiter la croissance du cache.
  • Appeler Compact ou Remove lorsque la mémoire disponible est limitée.

Le code suivant crée un MemoryCache de taille fixe sans unité accessible par injection de dépendances :

public class MyMemoryCache
{
    public MemoryCache Cache { get; } = new MemoryCache(
        new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
}

SizeLimit n’a pas d’unités. Les entrées mises en cache doivent spécifier la taille dans les unités qu’elles jugent les plus appropriées si la limite de taille du cache a été définie. Tous les utilisateurs d’une instance de cache doivent utiliser le même système d’unité. Une entrée ne sera pas mise en cache si la somme des tailles d’entrée mises en cache dépasse la valeur spécifiée par SizeLimit. Si aucune limite de taille de cache n’est définie, la taille de cache définie sur l’entrée est ignorée.

Ce code suivant inscrit MyMemoryCache auprès du conteneur d’injection de dépendances :

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddSingleton<MyMemoryCache>();

MyMemoryCache est créé en tant que cache de mémoire indépendant pour les composants qui connaissent ce cache de taille limitée et qui savent comment définir correctement la taille d’entrée de cache.

La taille de l’entrée de cache peut être définie à l’aide de la méthode d’extension SetSize ou de la propriété Size :

if (!_myMemoryCache.Cache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        .SetSize(1);

    // cacheEntryOptions.Size = 1;

    _myMemoryCache.Cache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
}

Dans le code précédent, les deux lignes mises en surbrillance permettent tous deux de définir la taille de l’entrée de cache. SetSize est fourni pour des raisons pratiques lors du chaînage d’appels à new MemoryCacheOptions().

MemoryCache.Compact

MemoryCache.Compact tente de supprimer le pourcentage spécifié du cache dans l’ordre suivant :

  • Tous les éléments expirés.
  • Éléments par priorité. Les éléments de priorité la plus basse sont supprimés en premier.
  • Objets les moins récemment utilisés.
  • Éléments dont l’expiration absolue est la plus ancienne.
  • Éléments dont l’expiration glissante est la plus ancienne.

Les éléments épinglés avec priorité NeverRemove ne sont jamais supprimés. Le code suivant supprime un élément de cache et appelle Compact pour supprimer 25 % des entrées mises en cache :

_myMemoryCache.Cache.Remove(CacheKeys.Entry);
_myMemoryCache.Cache.Compact(.25);

Pour plus d’informations, consultez la source Compact sur GitHub.

Mettre en cache les dépendances

L’exemple suivant montre comment faire expirer une entrée de cache si une entrée dépendante expire. Un CancellationChangeToken est ajouté à l’élément mis en cache. Quand Cancel est appelé sur le CancellationTokenSource, les deux entrées de cache sont supprimées :

public void OnGetCacheCreateDependent()
{
    var cancellationTokenSource = new CancellationTokenSource();

    _memoryCache.Set(
        CacheKeys.DependentCancellationTokenSource,
        cancellationTokenSource);

    using var parentCacheEntry = _memoryCache.CreateEntry(CacheKeys.Parent);

    parentCacheEntry.Value = DateTime.Now;

    _memoryCache.Set(
        CacheKeys.Child,
        DateTime.Now,
        new CancellationChangeToken(cancellationTokenSource.Token));
}

public void OnGetCacheRemoveDependent()
{
    var cancellationTokenSource = _memoryCache.Get<CancellationTokenSource>(
        CacheKeys.DependentCancellationTokenSource);

    cancellationTokenSource.Cancel();
}

L’utilisation d’un CancellationTokenSource permet de supprimer plusieurs entrées de cache en tant que groupe. Avec le modèle using dans le code ci-dessus, les entrées de cache créées à l’intérieur de l’étendue using héritent des déclencheurs et des paramètres d’expiration.

Remarques supplémentaires

  • L’expiration ne se produit pas en arrière-plan. Il n’existe aucun minuteur qui analyse activement le cache à la recherche d’éléments expirés. Toute activité dans le cache (Get, Set, Remove) peut déclencher une recherche en arrière-plan des éléments expirés. Un minuteur sur le CancellationTokenSource (CancelAfter) supprime également l’entrée et déclenche une recherche des éléments expirés. L’exemple suivant utilise CancellationTokenSource(TimeSpan) pour le jeton inscrit. Lorsque ce jeton se déclenche, il supprime immédiatement l’entrée et déclenche les rappels d’éviction :

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = DateTime.Now;
    
        var cancellationTokenSource = new CancellationTokenSource(
            TimeSpan.FromSeconds(10));
    
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .AddExpirationToken(
                new CancellationChangeToken(cancellationTokenSource.Token))
            .RegisterPostEvictionCallback((key, value, reason, state) =>
            {
                ((CancellationTokenSource)state).Dispose();
            }, cancellationTokenSource);
    
        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }
    
  • Lors de l’utilisation d’un rappel pour remplir à nouveau un élément de cache :

    • Plusieurs requêtes peuvent trouver la valeur de clé mise en cache vide, car le rappel n’est pas terminé.
    • Cela peut entraîner le remplissage de l’élément mis en cache par plusieurs threads.
  • Lorsqu’une entrée de cache est utilisée pour en créer une autre, l’enfant copie les jetons d’expiration et les paramètres d’expiration basés sur le temps de l’entrée parente. L’enfant n’expire pas lorsque l’entrée parente est supprimée ou mise à jour manuellement.

  • Utilisez PostEvictionCallbacks pour définir les rappels qui seront déclenchés après la suppression de l’entrée du cache.

  • Pour la plupart des applications, IMemoryCache est activé. Par exemple, l’appel de AddMvc, AddControllersWithViews, AddRazorPages, AddMvcCore().AddRazorViewEngine et de nombreuses autres méthodes Add{Service} dans Program.cs, active IMemoryCache. Pour les applications qui n’appellent pas l’une des méthodes Add{Service} précédentes, il peut être nécessaire d’appeler AddMemoryCache dans Program.cs.

Mise à jour du cache en arrière-plan

Utilisez un service en arrière-plan tel que IHostedService pour mettre à jour le cache. Le service en arrière-plan peut recalculer les entrées, puis les affecter au cache uniquement lorsqu’elles sont prêtes.

Ressources supplémentaires

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Principes de base de la mise en cache

La mise en cache peut améliorer considérablement les performances et la scalabilité d’une application en réduisant le travail nécessaire pour générer du contenu. La mise en cache fonctionne mieux avec les données qui changent rarement et sont coûteuses à générer. La mise en cache crée une copie des données qui peut être retournée beaucoup plus rapidement qu’à partir de la source. Les applications doivent être écrites et testées pour ne jamais dépendre des données mises en cache.

ASP.NET Core prend en charge plusieurs caches différents. Le cache le plus simple est basé sur IMemoryCache. IMemoryCache représente un cache stocké dans la mémoire du serveur web. Les applications exécutées sur une batterie de serveurs (plusieurs serveurs) doivent s’assurer que les sessions sont persistantes lorsqu’elles utilisent du cache en mémoire. Les sessions persistantes garantissent que les requêtes suivantes d’un client sont toutes envoyées au même serveur. Par exemple, les applications Azure Web utilisent Application Request Routing (ARR) pour acheminer toutes les requêtes suivantes vers le même serveur.

Les sessions non persistantes dans une batterie de serveurs web nécessitent un cache distribué pour éviter les problèmes de cohérence du cache. Pour certaines applications, un cache distribué peut prendre en charge un scale-out plus élevé qu’un cache en mémoire. L’utilisation d’un cache distribué décharge la mémoire cache vers un processus externe.

Le cache en mémoire peut stocker n’importe quel objet. L’interface de cache distribué est limitée au byte[]. Le cache en mémoire et le cache distribué stockent les éléments de cache sous forme de paires clé-valeur.

System.Runtime.Caching/MemoryCache

System.Runtime.Caching/MemoryCache (package NuGet) peut être utilisé avec :

  • .NET Standard 2.0 ou version ultérieure.
  • Une implémentation .NET qui cible .NET Standard 2.0 ou version ultérieure. Par exemple, ASP.NET Core 3.1 ou version ultérieure.
  • .NET Framework 4.5 ou ultérieur.

Microsoft.Extensions.Caching.Memory/IMemoryCache (décrit dans cet article) est recommandé, plutôt que System.Runtime.Caching/MemoryCache, car il est mieux intégré à ASP.NET Core. Par exemple, IMemoryCache fonctionne en mode natif avec l’injection de dépendances ASP.NET Core.

Utilisez System.Runtime.Caching/MemoryCache comme pont de compatibilité lors du portage du code d’ASP.NET 4.x vers ASP.NET Core.

Recommandations sur le cache

  • Le code doit toujours avoir une option de secours pour extraire des données et ne pas dépendre de la disponibilité d’une valeur mise en cache.
  • Le cache utilise une ressource rare, la mémoire. Limitez la croissance du cache :
    • N’utilisez pas d’entrée externe comme clés de cache.
    • Utilisez des expirations pour limiter la croissance du cache.
    • Utilisez SetSize, Size et SizeLimit pour limiter la taille du cache. Le runtime ASP.NET Core ne limite pas la taille du cache en fonction de la sollicitation de la mémoire. Il appartient au développeur de limiter la taille du cache.

Utiliser IMemoryCache

Avertissement

L’utilisation d’un cache de mémoire partagé à partir de l’injection de dépendances et l’appel de SetSize, Sizeou SizeLimit pour limiter la taille du cache peuvent entraîner l’échec de l’application. Lorsqu’une limite de taille est définie sur un cache, toutes les entrées doivent spécifier une taille lors de l’ajout. Cela peut entraîner des problèmes, car les développeurs peuvent ne pas avoir un contrôle total sur ce qui utilise le cache partagé. Lorsque vous utilisez SetSize, Size ou SizeLimit pour limiter le cache, créez un singleton de cache pour la mise en cache. Pour plus d’informations et un exemple, consultez Utiliser SetSize, Size et SizeLimit pour limiter la taille du cache. Un cache partagé est partagé par d’autres frameworks ou bibliothèques.

La mise en cache en mémoire est un service référencé à partir d’une application à l’aide de l’injection de dépendances. Interrogez l’instance IMemoryCache dans le constructeur :

public class HomeController : Controller
{
    private IMemoryCache _cache;

    public HomeController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }

Le code suivant utilise TryGetValue pour vérifier si une heure est dans le cache. Si aucune heure n’est mise en cache, une nouvelle entrée est créée et ajoutée au cache avec Set. La classe CacheKeys fait partie de l’exemple en téléchargement.

public static class CacheKeys
{
    public static string Entry => "_Entry";
    public static string CallbackEntry => "_Callback";
    public static string CallbackMessage => "_CallbackMessage";
    public static string Parent => "_Parent";
    public static string Child => "_Child";
    public static string DependentMessage => "_DependentMessage";
    public static string DependentCTS => "_DependentCTS";
    public static string Ticks => "_Ticks";
    public static string CancelMsg => "_CancelMsg";
    public static string CancelTokenSource => "_CancelTokenSource";
}
public IActionResult CacheTryGetValueSet()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Save data in cache.
        _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
    }

    return View("Cache", cacheEntry);
}

L’heure actuelle et l’heure mise en cache sont affichées :

@model DateTime?

<div>
    <h2>Actions</h2>
    <ul>
        <li><a asp-controller="Home" asp-action="CacheTryGetValueSet">TryGetValue and Set</a></li>
        <li><a asp-controller="Home" asp-action="CacheGet">Get</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreate">GetOrCreate</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAsynchronous">CacheGetOrCreateAsynchronous</a></li>
        <li><a asp-controller="Home" asp-action="CacheRemove">Remove</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbs">CacheGetOrCreateAbs</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbsSliding">CacheGetOrCreateAbsSliding</a></li>

    </ul>
</div>

<h3>Current Time: @DateTime.Now.TimeOfDay.ToString()</h3>
<h3>Cached Time: @(Model == null ? "No cached entry found" : Model.Value.TimeOfDay.ToString())</h3>

Le code suivant utilise la méthode d’extension Set pour mettre en cache les données pendant une durée relative sans créer l’objet MemoryCacheEntryOptions :

public IActionResult SetCacheRelativeExpiration()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Save data in cache and set the relative expiration time to one day
        _cache.Set(CacheKeys.Entry, cacheEntry, TimeSpan.FromDays(1));
    }

    return View("Cache", cacheEntry);
}

La valeur DateTime mise en cache reste dans le cache tant qu’il y a des requêtes dans le délai d’expiration.

Le code suivant utilise GetOrCreate et GetOrCreateAsync pour mettre les données en cache.

public IActionResult CacheGetOrCreate()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(3);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

public async Task<IActionResult> CacheGetOrCreateAsynchronous()
{
    var cacheEntry = await
        _cache.GetOrCreateAsync(CacheKeys.Entry, entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    return View("Cache", cacheEntry);
}

Le code suivant appelle Get pour récupérer l’heure mise en cache :

public IActionResult CacheGet()
{
    var cacheEntry = _cache.Get<DateTime?>(CacheKeys.Entry);
    return View("Cache", cacheEntry);
}

Le code suivant récupère ou crée un élément mis en cache avec une expiration absolue :

public IActionResult CacheGetOrCreateAbs()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

Un éléments mis en cache défini uniquement avec une expiration glissante risque de ne jamais expirer. Si l’élément mis en cache est accédé à plusieurs reprises dans l’intervalle d’expiration glissant, l’élément n’expire jamais. Combinez une expiration glissante avec une expiration absolue pour garantir l’expiration de l’élément. L’expiration absolue définit une limite supérieure sur la durée pendant laquelle l’élément peut être mis en cache tout en permettant à l’élément d’expirer plus tôt s’il n’est pas interrogé dans l’intervalle d’expiration glissant. Si l’intervalle d’expiration glissant ou le délai d’expiration absolu passent, l’élément est supprimé du cache.

Le code suivant récupère ou crée un élément mis en cache avec une expiration glissante et une expiration absolue :

public IActionResult CacheGetOrCreateAbsSliding()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SetSlidingExpiration(TimeSpan.FromSeconds(3));
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

Le code précédent garantit que les données ne seront pas mises en cache plus longtemps que l’heure absolue.

GetOrCreate, GetOrCreateAsync et Get sont des méthodes d’extension dans la classe CacheExtensions. Ces méthodes étendent la fonctionnalité de IMemoryCache.

MemoryCacheEntryOptions

L’exemple suivant :

  • Définit un délai d’expiration glissant. Les requêtes qui accèdent à cet élément mis en cache réinitialisent le délai d’expiration glissant.
  • Définit la priorité du cache sur CacheItemPriority.NeverRemove.
  • Définit un PostEvictionDelegate qui est appelé après la suppression de l’entrée du cache. Le rappel est exécuté sur un thread différent du code qui supprime l’élément du cache.
public IActionResult CreateCallbackEntry()
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        // Pin to cache.
        .SetPriority(CacheItemPriority.NeverRemove)
        // Add eviction callback
        .RegisterPostEvictionCallback(callback: EvictionCallback, state: this);

    _cache.Set(CacheKeys.CallbackEntry, DateTime.Now, cacheEntryOptions);

    return RedirectToAction("GetCallbackEntry");
}

public IActionResult GetCallbackEntry()
{
    return View("Callback", new CallbackViewModel
    {
        CachedTime = _cache.Get<DateTime?>(CacheKeys.CallbackEntry),
        Message = _cache.Get<string>(CacheKeys.CallbackMessage)
    });
}

public IActionResult RemoveCallbackEntry()
{
    _cache.Remove(CacheKeys.CallbackEntry);
    return RedirectToAction("GetCallbackEntry");
}

private static void EvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.CallbackMessage, message);
}

Utiliser SetSize, Size et SizeLimit pour limiter la taille du cache

Une instance MemoryCache peut éventuellement spécifier et appliquer une limite de taille. La limite de taille du cache n’a pas d’unité de mesure définie, car le cache n’a aucun mécanisme pour mesurer la taille des entrées. Si la limite de taille du cache est définie, toutes les entrées doivent spécifier la taille. Le runtime ASP.NET Core ne limite pas la taille du cache en fonction de la sollicitation de la mémoire. Il appartient au développeur de limiter la taille du cache. La taille spécifiée est en unités choisies par le développeur.

Par exemple :

  • Si l’application web a principalement mis en cache des chaînes, chaque taille d’entrée de cache peut être la longueur de la chaîne.
  • L’application peut spécifier la taille de toutes les entrées à 1, et la limite de taille correspond au nombre d’entrées.

Si SizeLimit n’est pas défini, la taille du cache augmente sans limite. Le runtime ASP.NET Core ne découpe pas le cache lorsque la mémoire système est faible. Les applications doivent être conçues pour :

  • Limiter la croissance du cache.
  • Appelez Compact ou Remove lorsque la mémoire disponible est limitée :

Le code suivant crée un MemoryCache de taille fixe sans unité accessible par injection de dépendances :

// using Microsoft.Extensions.Caching.Memory;
public class MyMemoryCache 
{
    public MemoryCache Cache { get; private set; }
    public MyMemoryCache()
    {
        Cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
    }
}

SizeLimit n'a pas d’unités. Les entrées mises en cache doivent spécifier la taille dans les unités qu’elles jugent les plus appropriées si la limite de taille du cache a été définie. Tous les utilisateurs d’une instance de cache doivent utiliser le même système d’unité. Une entrée ne sera pas mise en cache si la somme des tailles des entrées mises en cache dépasse la valeur spécifiée par SizeLimit. Si aucune limite de taille de cache n’est définie, la taille de cache définie sur l’entrée est ignorée.

Ce code suivant inscrit MyMemoryCache auprès du conteneur d’injection de dépendances.

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddSingleton<MyMemoryCache>();
}

MyMemoryCache est créé en tant que cache de mémoire indépendant pour les composants qui connaissent ce cache de taille limitée et qui savent comment définir correctement la taille d’entrée de cache.

Le code suivant utilise MyMemoryCache :

public class SetSize : PageModel
{
    private MemoryCache _cache;
    public static readonly string MyKey = "_MyKey";

    public SetSize(MyMemoryCache memoryCache)
    {
        _cache = memoryCache.Cache;
    }

    [TempData]
    public string DateTime_Now { get; set; }

    public IActionResult OnGet()
    {
        if (!_cache.TryGetValue(MyKey, out string cacheEntry))
        {
            // Key not in cache, so get data.
            cacheEntry = DateTime.Now.TimeOfDay.ToString();

            var cacheEntryOptions = new MemoryCacheEntryOptions()
                // Set cache entry size by extension method.
                .SetSize(1)
                // Keep in cache for this time, reset time if accessed.
                .SetSlidingExpiration(TimeSpan.FromSeconds(3));

            // Set cache entry size via property.
            // cacheEntryOptions.Size = 1;

            // Save data in cache.
            _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
        }

        DateTime_Now = cacheEntry;

        return RedirectToPage("./Index");
    }
}

La taille de l’entrée de cache peut être définie par Size ou par les méthodes d’extension SetSize :

public IActionResult OnGet()
{
    if (!_cache.TryGetValue(MyKey, out string cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now.TimeOfDay.ToString();

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Set cache entry size by extension method.
            .SetSize(1)
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Set cache entry size via property.
        // cacheEntryOptions.Size = 1;

        // Save data in cache.
        _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
    }

    DateTime_Now = cacheEntry;

    return RedirectToPage("./Index");
}

MemoryCache.Compact

MemoryCache.Compact tente de supprimer le pourcentage spécifié du cache dans l’ordre suivant :

  • Tous les éléments expirés.
  • Éléments par priorité. Les éléments de priorité la plus basse sont supprimés en premier.
  • Objets les moins récemment utilisés.
  • Éléments dont l’expiration absolue est la plus ancienne.
  • Éléments dont l’expiration glissante est la plus ancienne.

Les éléments épinglés avec priorité NeverRemove ne sont jamais supprimés. Le code suivant supprime un élément de cache et appelle Compact :

_cache.Remove(MyKey);

// Remove 33% of cached items.
_cache.Compact(.33);   
cache_size = _cache.Count;

Pour plus d’informations, consultez la source Compact sur GitHub.

Mettre en cache les dépendances

L’exemple suivant montre comment faire expirer une entrée de cache si une entrée dépendante expire. Un CancellationChangeToken est ajouté à l’élément mis en cache. Quand Cancel est appelé sur CancellationTokenSource, les deux entrées de cache sont supprimées.

public IActionResult CreateDependentEntries()
{
    var cts = new CancellationTokenSource();
    _cache.Set(CacheKeys.DependentCTS, cts);

    using (var entry = _cache.CreateEntry(CacheKeys.Parent))
    {
        // expire this entry if the dependant entry expires.
        entry.Value = DateTime.Now;
        entry.RegisterPostEvictionCallback(DependentEvictionCallback, this);

        _cache.Set(CacheKeys.Child,
            DateTime.Now,
            new CancellationChangeToken(cts.Token));
    }

    return RedirectToAction("GetDependentEntries");
}

public IActionResult GetDependentEntries()
{
    return View("Dependent", new DependentViewModel
    {
        ParentCachedTime = _cache.Get<DateTime?>(CacheKeys.Parent),
        ChildCachedTime = _cache.Get<DateTime?>(CacheKeys.Child),
        Message = _cache.Get<string>(CacheKeys.DependentMessage)
    });
}

public IActionResult RemoveChildEntry()
{
    _cache.Get<CancellationTokenSource>(CacheKeys.DependentCTS).Cancel();
    return RedirectToAction("GetDependentEntries");
}

private static void DependentEvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Parent entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message);
}

L’utilisation d’un CancellationTokenSource permet de supprimer plusieurs entrées de cache en tant que groupe. Avec le modèle using dans le code ci-dessus, les entrées de cache créées à l’intérieur du bloc using héritent des déclencheurs et des paramètres d’expiration.

Remarques supplémentaires

  • L’expiration ne se produit pas en arrière-plan. Il n’existe aucun minuteur qui analyse activement le cache à la recherche d’éléments expirés. Toute activité dans le cache (Get, Set, Remove) peut déclencher une recherche en arrière-plan des éléments expirés. Un minuteur sur le CancellationTokenSource (CancelAfter) supprime également l’entrée et déclenche une recherche des éléments expirés. L’exemple suivant utilise CancellationTokenSource(TimeSpan) pour le jeton inscrit. Lorsque ce jeton se déclenche, il supprime immédiatement l’entrée et déclenche les rappels d’éviction :

    public IActionResult CacheAutoExpiringTryGetValueSet()
    {
        DateTime cacheEntry;
    
        if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
        {
            cacheEntry = DateTime.Now;
    
            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
    
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .AddExpirationToken(new CancellationChangeToken(cts.Token));
    
            _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
        }
    
        return View("Cache", cacheEntry);
    }
    
  • Lors de l’utilisation d’un rappel pour remplir à nouveau un élément de cache :

    • Plusieurs requêtes peuvent trouver la valeur de clé mise en cache vide, car le rappel n’est pas terminé.
    • Cela peut entraîner le remplissage de l’élément mis en cache par plusieurs threads.
  • Lorsqu’une entrée de cache est utilisée pour en créer une autre, l’enfant copie les jetons d’expiration et les paramètres d’expiration basés sur le temps de l’entrée parente. L’enfant n’expire pas lorsque l’entrée parente est supprimée ou mise à jour manuellement.

  • Utilisez PostEvictionCallbacks pour définir les rappels qui seront déclenchés après la suppression de l’entrée du cache. Dans l’exemple de code, CancellationTokenSource.Dispose() est appelé pour libérer les ressources non gérées utilisées par le CancellationTokenSource. Toutefois, le fichier CancellationTokenSource n’est pas supprimé immédiatement, car il est toujours utilisé par l’entrée du cache. Le CancellationToken est transmis à MemoryCacheEntryOptions pour créer une entrée de cache qui expire après un certain temps. Dispose ne doit donc pas être appelé tant que l’entrée du cache n’est pas supprimée ou expirée. L’exemple de code appelle la méthode RegisterPostEvictionCallback pour inscrire un rappel qui sera appelé lorsque l’entrée du cache est supprimée, et il élimine le CancellationTokenSource dans ce rappel.

  • Pour la plupart des applications, IMemoryCache est activé. Par exemple, l’appel de AddMvc, AddControllersWithViews, AddRazorPages, AddMvcCore().AddRazorViewEngine et de nombreuses autres méthodes Add{Service} dans ConfigureServices, active IMemoryCache. Pour les applications qui n’appellent pas l’une des méthodes Add{Service} précédentes, il peut être nécessaire d’appeler AddMemoryCache dans ConfigureServices.

Mise à jour du cache en arrière-plan

Utilisez un service en arrière-plan tel que IHostedService pour mettre à jour le cache. Le service en arrière-plan peut recalculer les entrées, puis les affecter au cache uniquement lorsqu’elles sont prêtes.

Ressources supplémentaires