Reprendre la main sur la gestion des caches avec “Output Caching” et .Net 7

Dans un monde qui va de plus en plus vite, la capacité d’un système ou d’une application à apporter des réponses rapides et cohérentes, est un élément crucial pour en mesurer son succès et sa performance.
Dans leur « quête vers le sommet », les développeurs disposent d’un ensemble d’outils et de pratiques pour y arriver : adopter les bonnes pratiques du code, optimiser ses requêtes d’accès aux données, etc. Cependant, dans certains scénarios, ils se trouvent face à des contraintes où l’optimisation du code montre des limites.
Pour mieux comprendre, prenons l’exemple des sites de e-commerce qui, en période des soldes, voient le temps d’affichage du catalogue des produits disponibles devenir de plus en plus long. Dans la majorité des cas, cette latence est le résultat du nombre élevé des requêtes, mais aussi du fait que le système exécute la même routine pour chaque demande pour obtenir parfois le même résultat. C’est dans ce cadre qu’une pratique très connue entre en jeu : le système de mise en cache.
La mise en cache est une pratique qui consiste à stocker les données transitoires (ou une partie d’entre elles) sur des supports de stockage rapides et hautement disponibles, de manière à ce que les futures demandes pour ces mêmes données soient traitées plus rapidement.
Différentes technologies de cache peuvent être utilisées et cela à plusieurs niveaux comme mettre en cache les données les plus utilisées, mettre en cache les résultats des calculs les plus gourmands ou tout simplement mettre en cache les résultats de sortie des réponses HTTP.
Dans cet article, nous vous proposons de découvrir une nouvelle ressource ASP.NET Core pour optimiser vos caches de sortie : le nouveau middleware « Output Caching ».
Dans les précédentes versions d’ASP.NET Core, la mise en cache des réponses renvoyées par les points de terminaison était assurée par le middleware « Response caching », qui est basé sur une interprétation stricte des en-têtes de mise en cache HTTP. En utilisant des en-têtes tels que « Cache-Control » et « no-cache », le consommateur indique au middleware la stratégie de cache à adopter.
Vous apprendrez ici pourquoi une nouvelle implémentation, qui porte le nom d’« Output Caching », était nécessaire.
Configurer Output Caching pour une application ASP.NET CoreCore
L’installation et la configuration de l’Output Caching sont relativement simples. Il suffit de s’assurer que vous disposez de la dernière version du SDK .NET 7.0 (au moment d’écrire cet article nous avons utilisé la version 7.0.0-rc2). Si ce n’est pas encore le cas vous pouvez la télécharger en cliquant sur ce lien : https://dotnet.microsoft.com/en-us/download/dotnet/7.0
Voici une configuration de base d’une minimal API avec Output Caching :
Program.cs
- AddOutputCache() : ajoute les services nécessaires à la gestion de cache au niveau de l’IoC.
- UseOutputCache() : ajoute la gestion de cache au niveau du pipeline des middlewares ASP.Net Core.
Si nécessaire, vous pouvez remplacer les valeurs par défaut par des valeurs de votre choix.
- SizeLimit : il s’agit de la taille maximale autorisée par le stockage du cache. Si cette limite est atteinte, aucune nouvelle entrée ne sera autorisée par IOutputCacheStore.
- DefaultExpirationTimeSpan : un cache aura une période d’expiration par défaut égale à 1 minute. Au-delà, le cache sera automatiquement détruit.
Activer la mise en cache pour les point de terminaison
Activer la mise en cache pour toutes les routes en utilisant une stratégie de base :
Désactiver la mise en cache pour toutes les routes en utilisant une stratégie de base :
La méthode CacheOutput permet de configurer l’utilisation du mécanisme de mise en cache pour une route spécifique :
HTTP Request et variation de cache
Mise en cache avec SetVaryByQuery
Pour identifier l’unicité d’une requête entrante, le comportement par défaut consiste à utiliser le chemin complet de l’URL ainsi que l’ensemble des paramètres de la requête.
Si la valeur d’un paramètre de requête change ou lorsqu’un nouveau paramètre de requête est ajouté, le cache n’est pas atteint et une nouvelle entrée est donc ajoutée.
Pour illustrer cela, imaginons que nous disposons de la route suivante qui renvoie la liste des « évènements » et souhaitons les filtrer par ville :
Pour chacune des requêtes ci-dessous, nous aurons une mise en cache d’entité différente :
- https://localhost:5269/\> GET /events
- https://localhost:5269/\> GET /events?city=paris
- https://localhost:5269/\> GET /events?city=lyon
Pour pousser plus loin le concept, nous ajoutons une variable « partnerToken » qui nous permettra de récompenser les partenaires qui exposent nos évènements sur leur plateforme.
Comme le montre le code, cette valeur n’influe pas le résultat de retour :
- https://localhost:5269/>GET/events?partnerToken =”{12A6E9DC-0FED-40A1-9769-DA71103327B4}”
- https://localhost:5269/>GET/events?city=paris&providerToken=”{8F7698FA-9F23-4DD7-B8DC-93BB4B9C68DB}”
- https://localhost:5269/>GET/events?city=lyon&providerToken=”{7A785CD3-3F23-4CBB-AEE6-4979D2291339}”
Sans aucune configuration préalable de notre part, chaque route représente une clé unique et fait l’objet d’une mise en cache par le middleware, ce qui n’est pas forcément le comportement souhaité, étant donné que la variable « partnerToken » n’a aucun impact sur le résultat de la demande mais qui sera utiliser par d’autre middleware en amont comme le Rate Limiter .
Pour remédier à cela et spécifier le(s) paramètre(s) à prendre en considération dans la gestion de notre cache, il est possible de surcharger la méthode « CacheOutput()» et d’utiliser la méthode d’extensions « OutputCachePolicyBuilder.VaryByQuery » comme le montre l’exemple ci-dessous :
Mise en cache avec SetVaryByHeader
A l’instar des paramètres de requête, il est également possible de varier notre cache par l’utilisation des en-têtes HTTP.
Il existe d’autres types et mécanisme de variations comme « SetVaryByHost » et « SetVaryByRouteValue ».
HTTP Request et variation de cache
Nous avons vu comment configurer les caches au niveau des différentes routes de notre application, ce qui peut être une source de duplication de code, compliquant dans un second temps la maintenance et l’évolution de notre code.
Pour éviter de dupliquer les paramètres de votre cache au niveau de plusieurs routes, il est possible de regrouper ces paramètres dans une stratégie qui sera référencée ou associée à une ou plusieurs routes.
Il existe deux types de stratégies :
- Les stratégies de base.
- Les stratégies personnalisées ou nommées.
Les stratégies de base
Il s’agit de la stratégie utilisée par default pour l’ensemble des routes déclarées et qui ne disposent pas de configuration spécifique de cache via la méthode / attribut CacheOutput.
Ces stratégies ne portent pas de nom. Elles sont injectées par la fonction « AddBasePolicy ».
L’exemple ci-dessous montre comment désactiver la mise en cache pour toutes les routes :
Les stratégies personnalisées
Contrairement aux stratégies de base, ces stratégies sont personnalisées : elle doivent porter un nom. Elles ne sont pas appliquées à toutes les routes et peuvent être associées à une route via la méthode / attribut « CacheOutput ».
L’exemple ci-dessous montre comment activer la mise en cache pour une route spécifique :
Tags & purge de cache
Comment mentionné au début de cet article, les éléments qui constituent votre cache ont une durée d’expiration égale à 1 minute. Cette durée peut toutefois être modifiée selon les scénarios.
En revanche, dans certains scénarios, il est nécessaire d’invalider ou de purger des éléments de notre cache, notamment quand la ressource en question a été mise à jour.
Pour cela, il faudra :
- « Tagguer » ces ressources. Les tags sont un mécanisme qui permet de regrouper et d’identifier un ensemble d’éléments dans notre cache.
- Utiliser la méthode « EvictByTagAsync » qui supprimera l’ensemble des éléments mis en cache associés à ce tag.
L’exemple ci-dessous montre comment purger le cache suite à l’ajout ou à la modification d’un évènement :
Mise à l’échelle et magasin de cache externe
Par défaut, Output caching utilise un cache local de type « In-memory ». Voici un extrait du code de la configuration par défaut :
Vous trouverez le code complet ici : OutputCacheServiceCollectionExtensions
Cette solution rapide et simple montre toutefois quelques inconvénients :
- En utilisant un cache local, toute tentative de mise à l’échelle est impossible, car le cache n’est pas partagé entre les différentes instances.
- La consommation de la mémoire impactera la performance de l’application.
- Si l’application est destinée à un déploiement dans le Cloud, cela entrainera des coûts élevés de consommation de mémoire.
Une des solutions sera de configurer notre cache comme un service externe.
Dans l’exemple ci-dessous, nous allons configurer et utiliser « azure-redis-cache » comme magasin de cache externe.
A noter : certaines parties de code sont incomplètes afin de faciliter la lisibilité de l’article.
1 – Ajoutez une référence du package à votre solution
Microsoft.Extensions.Caching.StackExchangeRedis
2- Implémentez « IDistributedCache » en tant que « RedisOutputCacheStore ».
3- Enregistrez votre cache à l’aide d’une méthode d’extension ServiceCollection « AddRedisOutputCache() ».
Dans votre fichier « Program.cs », remplacez « builder.Services.AddOutputCache()» par :
Aller plus loin sur Output Caching
Vous avez pu voir lors de cet article les différents usages que nous offre l’Output Caching pour améliorer la performance de nos applications.
⚠️ Attention toutefois, rappelons que l’intégration d’un cache ne doit pas être utilisée pour résoudre des problèmes de performance liés à des choix de design (conception) ou d’implémentation, mais plutôt pour améliorer le rendement de nos applications.
Vous pouvez trouver d’autres exemples ici : Github Exemples
Retrouvez tous les articles de cette série autour de .NET :
- Découvrez les nouveautés de C#11
- Nouveauté d’ASP .NET Core 7 : le middleware Rate Limiting
- Minimal API dans ASP.NET Core