La mise en cache des données peut permettre de grandement améliorer les performances d’un site web. ASP.NET Core 2 propose plusieurs manières de gérer la mise en cache coté client et coté serveur.
Le protocole HTTP prévoit le header cache-control dans les réponses pour que le serveur puisse indiquer au navigateur s’il doit mettre en cache la ressource et pour combien de temps.
En ASP.NET Core on peut définir ce header en mettant l’attribut ResponseCache sur une action ou un controller.
[ResponseCache(Duration = 3600, VaryByQueryKeys = new string[] { "format" })]
public async Task<IActionResult> WhatTimeIsIt(string format)
{
await Task.Delay(2000);
return Content(DateTime.Now.ToString(format));
}
Les paramètres de cet attribut nous permettent d’indiquer la durée de mise en cache, et si la réponse varie en fonction d’éléments de la query ou en fonction des headers.
Si on utilise fréquemment la même logique de mise cache, on peut déclarer un profile de cache dans le fichier startup:
services.AddMvc(options =>
{
options.CacheProfiles.Add("VaryByName", new CacheProfile()
{
Duration = 60 * 60 * 24,
VaryByQueryKeys = new string [] { "name" }
});
options.CacheProfiles.Add("1H", new CacheProfile()
{
Duration = 60 * 60
});
options.CacheProfiles.Add("DontCache", new CacheProfile()
{
NoStore = true
});
});
On peut ensuite appliquer un profile directement sur un controller ou une action :
[ResponseCache(CacheProfileName = "VaryByName")]
public async Task<IActionResult> GetProduct(string name)
{
...
}
Une autre manière d’ajouter ce header est de le placer directement dans la réponse HTTP. Par exemple si on souhaite mettre en cache les fichiers statiques, on peut le faire dans les options du middleware StaticFiles :
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
var headers = ctx.Context.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromDays(30)
};
}
});
...
}
Ces headers de gestion du cache du navigateur n’ont rien de nouveau, là où ASP.NET Core propose quelque chose d'intéressant c’est avec le middleware ResponseCaching qui va mettre les réponses en cache coté serveur en fonction du header cache-control des réponses HTTP. Pour utiliser le ResponseCaching, il faut l'ajouter aux services et dans la pipeline des middlewares :
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCaching();
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseResponseCaching();
...
}
La logique de ce middleware est que lorsqu’on estime qu’une ressource n’est pas souvent mise à jour et peut être mise en cache par le navigateur, autant également la mettre en cache sur le serveur pour ne pas avoir à la régénérer lorsqu’un nouvel utilisateur la demandera.
Et bien évidement ce middleware respecte le header cache-control des requêtes et ne renvoie pas de réponses provenant du cache quand le client demande une ressource à jour. (En faisant un F5 dans son navigateur par exemple)
Par contre il faut faire attention à ne pas utiliser ce middleware pour mettre en cache des pages pouvant varier d’un utilisateur à l’autre. Ce n'est donc pas une méthode de mise en cache adaptée à tous les cas.
Dans ces cas où mettre en cache les réponses HTTP n’est pas le plus adéquat, on va préférer garder en cache des données servant à générer ces réponses. Par exemple quand ces données ne changent pas souvent, qu’elles proviennent d’un webservice mettant du temps à répondre ou d’une requête SQL complexe.
Plutôt que de stocker ces données en session ou dans une propriété statique, on peut utiliser le MemoryCache de ASP.NET Core.
Pour cela il faut ajouter le service dans startup :
services.AddMemoryCache();
Ce qui nous donne accès à l’interface IMemoryCache qui permet de manipuler le cache mémoire sous forme de paires clé/valeur avec la possibilité de définir des durées d’expiration des objets mis en cache.
IMemoryCache _memoryCache;
public async Task<IActionResult> Index()
{
var model = _memoryCache.Get<MyModel>("ModelKey");
if (model == null)
{
model = await GetModelFromDatabase();
_memoryCache.Set("ModelKey", model, new MemoryCacheEntryOptions { SlidingExpiration= new TimeSpan(0, 30, 0) });
}
return View(model);
}
On peut définir une expiration absolue (par exemple dans 30 minutes), ou comme dans l’exemple ci-dessus, une expiration glissante. Dans ce cas la donnée est supprimée du cache si elle n’est pas utilisée pendant la durée définie. On a aussi la possibilité d’invalider un enregistrement du MemoryCache avec un CancellationToken :
var tokenSource = new CancellationTokenSource();
var token = new CancellationChangeToken(tokenSource.Token);
_memoryCache.Set("key", item, new MemoryCacheEntryOptions().AddExpirationToken(token));
…
//invalide le cache
tokenSource.Cancel();
On peut ainsi gérer plus finement ce qu’on met en cache, pour combien de temps, mais si on souhaite respecter le header cache-control des requêtes il faudra le gérer nous-même.
Parfois on peut vouloir mettre en cache une partie du HTML généré parce qu’il est commun à plusieurs pages et change peu (un menu par exemple). Pour faire cela on peut utiliser le CacheTagHelper de ASP.NET Core. Il suffit à l'intérieur d'une vue d’entourer la portion de HTML d’une balise cache :
<cache expires-sliding="@TimeSpan.FromHours(1)" vary-by-user="true" vary-by-route="controller">
<div>
@User.Identity.Name connecté depuis @DateTime.Now
</div>
</cache>
ASP.NET Core utilise un MemoryCache pour enregistrer le HTML contenu à l'intérieur de cette balise. Lorsque la vue est rappelé, le contenu de cette balise est récupéré du cache à la place d'être régénéré.
Comme avec le MemoryCache on peut définir une période de mise cache absolue ou glissante, et on peut via les attributs de cette balise paramétrer à quelle granularité la donnée doit être mise en cache.
Les attributs vary-by-header et vary-by-query fonctionnent comme dans le header ResponseCache. Mais cette balise propose de possibilités encore plus fines. Par exemple l’attribut vary-by-user permet d’avoir une mise en cache par utilisateur. L'attribut vary-by-route permet d'indiquer si le cache dépend des paramètres de la route.
Il y a des cas où le cache mémoire n’est pas adapté. Par exemple si notre site est hébergé sur un IIS avec un pool recyclé toutes les 24h et qu’on veut garder un cache plus long, ou quand notre site est hébergé sur plusieurs serveur et qu’on a besoin que ces serveurs partagent le même cache.
Pour répondre à ces besoins, ASP.NET Core nous propose le DistributedCache. Il existe de base deux implémentations, une utilisant SQL Server et une utilisant Redis. Mais ASP.NET Core étant simple à étendre il est facile de trouver d’autres implémentations ou d’en créer une.
Pour utiliser le DistributedCache avec SQL Server il faut commencer par créer la table de cache dans notre base SQL. On peut faire ça avec la commande dotnet cli suivante :
dotnet sql-cache create "Data Source=(localdb)\v11.0;Initial Catalog=DistCache;Integrated Security=True;" dbo MaTableDeCache
Ou directement en SQL
CREATE TABLE [dbo].[MaTableDeCache](
[Id] [nvarchar](449) NOT NULL,
[Value] [varbinary](max) NOT NULL,
[ExpiresAtTime] [datetimeoffset](7) NOT NULL,
[SlidingExpirationInSeconds] [bigint] NULL,
[AbsoluteExpiration] [datetimeoffset](7) NULL,
CONSTRAINT [pk_Id] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
CREATE NONCLUSTERED INDEX [Index_ExpiresAtTime] ON [dbo].[MaTableDeCache]
(
[ExpiresAtTime] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF,
ONLINE = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
Une fois notre table créée il faut ajouter le service DistributedCache :
services.AddDistributedSqlServerCache(o =>
{
o.ConnectionString = Configuration["ConnectionString"];
o.SchemaName = "dbo";
o.TableName = "MaTableDeCache";
});
Cela nous donne accès à l'interface IDistributedCache qui permet comme IMemoryCache de manipuler les données en cache sous forme de paires clé/valeur.
Mais il y a quelques différences dans l’utilisation du DistributedCache par rapport au MemoryCache.
IDistributedCache _cache;
public async Task<IActionResult> Index()
{
MyModel model = null;
var json = await _cache.GetStringAsync("ModelKey");
if (json != null)
model = JsonConvert.DeserializeObject<MyModel>(json);
if (model == null)
{
model = await GetModelFromDatabase();
await _cache.SetStringAsync("ModelKey", JsonConvert.SerializeObject(model), new DistributedCacheEntryOptions { SlidingExpiration = new TimeSpan(0, 30, 0) });
}
return View(model);
}
Et comme avec le MemoryCache, on peut aussi mettre en cache directement du HTML avec la balise distributed-cache :
<distributed-cache name="NomUtilisateur" expires-sliding="@TimeSpan.FromHours(1)" vary-by-user="true">
<div>
@User.Identity.Name connecté depuis @DateTime.Now
</div>
</distributed-cache>
La seule différence avec la balise cache (en dehors du nom) est l’attribut obligatoire name qui est utilisé dans la génération de la clé. Cet attribut peut être utilisé pour faire varier le cache selon un critère supplémentaire, on peut par exemple y mettre le timestamp de dernière mise à jour des données qu’on affiche.
Un problème commun à ces différentes manière de gérer le cache est qu'il n'est pas facile d'invalider un cache lorsqu'une donnée est mise à jour. Mais malgré cet inconvénient les différentes façon de gérer un cache coté serveur proposées par ASP.NET Core peuvent répondre aux besoin de la plus part des applications, et peuvent aider à l'amélioration des performances d'un site web.
Commentaires :
Aucun commentaires pour le moment
Laissez un commentaire :