Intermédiaire

[ASP.NET Core] Gestion des sessions

ASP.NET logoPour ceux que ne connaissent pas ce mécanisme, il s’agit simplement d’une API permettant de stocker et de récupérer des informations relatives à un utilisateur. Le stockage peut se faire en mémoire sur le serveur, les données étant persistées entre les requêtes HTTP d’un utilisateur, pendant toute la durée de sa session de navigation. Elles peuvent également être persistées pour une période plus longue encore, ce cas de figure nécessitant toutefois la mise en place d’une base de données avec un schéma spécifique.

Un gestionnaire des sessions ?

Dans la majorité des cas, la session reste utilisée en tant que cache mémoire afin de limiter le nombre d’accès à une base de données. Jusqu’à présent, j’avais tendance à déconseiller à mes clients l’usage direct de la session ASP.NET, et je préconisais à la place une solution maison propre (sous la forme d’une interface ICacheService) qui encapsule le stockage vers le HttpContext.Cache, HttpContext.Items ou HttpContext.Session selon les cas. Ces trois types de cache ayant pour inconvénient de ne pas être masqués par des abstractions propres, et donc offrir une testabilité très limitée pour le code qui les utilise.

Avec ASP.NET Core, le fonctionnement de la session est plus propre. Il est automatiquement encapsulé derrière des éléments qui peuvent être utilisé librement sans impacter la testabilité du code. De plus, le gestionnaire de session est maintenant distribué sous la forme d’un middleware, donc si vous n’avez pas besoin de ce gestionnaire, il suffit de ne pas le référencer pour ne pas alourdir inutilement votre application

Ce billet se base sur les APIs telles qu’elles sont exposées par la release candidate 1 d’ASP.NET Core. Il est possible que ces APIs diffèrent légèrement d’ici à la sortie finale du produit.

Installer le gestionnaire de session

Comme je l’ai indiqué plus tôt dans ce billet, le gestionnaire des sessions d’ASP.NET Core n’est pas automatiquement importé dans vos applications, et ce, même si vous référencez une bibliothèque de haut niveau telle que ASP.NET Core MVC.

La première étape est donc d’ajouter le support de la session dans votre application. Ce support est apporté par le paquet NuGet Microsoft.AspNet.Session.

Ce seul paquet n’est pas suffisant, il faut également tirer un autre paquet qui va correspondre au mode de persistance que devra utiliser le gestionnaire de sessions. Dans un premier temps, nous allons nous intéresser à la persistance en mémoire. Ce mécanisme est porté par le paquet NuGet Microsoft.Extensions.Caching.Memory.

Voici donc un exemple de fichier project.json que je vais utiliser pour la suite de ce billet.

Une fois ces dépendances ajoutées, il faut passer à la configuration du gestionnaire de session. Cela se fait en deux étapes.

Tout d’abord, l’instruction AddCaching permet d’enregistrer le gestionnaire de cache en mémoire (InProc). Ceci n’est pas propre au gestionnaire des sessions, c’est grâce à cette instruction que les services IMemoryCache et IDistributedCache vont être enregistrés dans le conteneur IoC. C’est cette seconde interface qui est utilisée par le gestionnaire de session. Dans le cas d’un cache en mémoire (le paquet Microsoft.Extensions.Caching.Memory), on ne peut pas vraiment parlé de cache distribué. Néanmoins, d’autres paquets offrent vraiment un fonctionnement de cache distribué : Microsoft.Extensions.Caching.SqlServer ou Microsoft.Extensions.Caching.Redis.

Dans un second temps, l’instruction AddSession va enregistrer le seul service introduit par le paquet Microsoft.AspNet.Session : ISessionStore. L’ensemble du gestionnaire de sessions utilise le modèle d’options d’ASP.NET Core. Le POCO utilisé ici est de type SessionOptions. Dans l’exemple ci-dessous, j’utilise deux propriétés de ce modèle pour régler la durée de ma session à 30 minutes au lieu de 20 minutes (le temps par défaut), et pour définir le nom du cookie à utiliser. Les données placées dans le cache le seront donc avec une expiration glissante sur 30 minutes.

public void ConfigureServices(IServiceCollection services)
{
    services.AddCaching();
    services.AddSession(options => 
        { 
            options.IdleTimeout = TimeSpan.FromMinutes(30); 
            options.CookieName = ".Session";
        });

    services.AddMvc();
}

Le POCO SessionOptions permet de régler finement la manière dont le cookie de session est émis. Il peut par exemple être émis pour un domaine spécifique, sous un chemin (url relative) spécifique, ou uniquement pour une zone sécurisée (HTTPS). Sa définition complète est visible ci-dessous.

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.AspNetCore.Session;

namespace Microsoft.AspNetCore.Builder
{
    /// <summary>
    /// Represents the session state options for the application.
    /// </summary>
    public class SessionOptions
    {
        /// <summary>
        /// Determines the cookie name used to persist the session ID.
        /// Defaults to <see cref="SessionDefaults.CookieName"/>.
        /// </summary>
        public string CookieName { get; set; } = SessionDefaults.CookieName;

        /// <summary>
        /// Determines the domain used to create the cookie. Is not provided by default.
        /// </summary>
        public string CookieDomain { get; set; }

        /// <summary>
        /// Determines the path used to create the cookie.
        /// Defaults to <see cref="SessionDefaults.CookiePath"/>.
        /// </summary>
        public string CookiePath { get; set; } = SessionDefaults.CookiePath;

        /// <summary>
        /// Determines if the browser should allow the cookie to be accessed by client-side JavaScript. The
        /// default is true, which means the cookie will only be passed to HTTP requests and is not made available
        /// to script on the page.
        /// </summary>
        public bool CookieHttpOnly { get; set; } = true;

        /// <summary>
        /// The IdleTimeout indicates how long the session can be idle before its contents are abandoned. Each session access
        /// resets the timeout. Note this only applies to the content of the session, not the cookie.
        /// </summary>
        public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromMinutes(20);
    }
}

La seconde étape consiste à enregistrer le middleware chargé de la gestion des sessions. Attention, les middlewares sont exécutés sous la forme d’un tunnel. Leur ordre d’enregistrement est donc important : si un middleware doit être exécuté avant un autre, il faut qu’il soit enregistré avant. Ainsi, pour que le gestionnaire de session puisse fonctionner et que les informations de session soient réellement utilisables dans vos contrôleurs MVC, il faut enregistrer son middleware avant le middleware MVC.

public void Configure(IApplicationBuilder app)
{
    app.UseIISPlatformHandler();

    app.UseSession();
    app.UseMvcWithDefaultRoute();
}

Point important : les événements Session_Start et Session_End qui étaient utilisables via le global.asax de vos applications ASP.NET ne sont pas utilisables avec ASP.NET Core. En effet, ce nouveau framework est très orienté cloud. Or, idéalement, il n’y a pas de garantie que les utilisateurs tombent tout le temps sur la même machine web dans un environnement cloud. Ces événements qui marquent le début et la fin de session n’ont alors aucun sens dans un tel environnement. Si l’utilisateur passe de machine en machine et que la session est stockée en mémoire (l’approche que je vous recommande), l’événement Session_Start sera levé sans cesse.

Utiliser le gestionnaire de session

Il y a plusieurs façons d’utiliser le gestionnaire de session avec ASP.NET Core. Tout d’abord, il y à l’approche compatible avec les versions précédentes d’ASP.NET, en utilisant la propriété Session de l’objet HttpContext. L’exemple suivant illustre cette approche.

Les méthodes GetString et SetString sont des simples méthodes d’extensions sur l’inteface ISession. Elles encapsulent des appels aux méthodes TryGet et Set qui utilisent des champs de byte pour stocker ou récupérer des données. Si vous souhaitez utiliser d’autres types que string et int qui sont les deux types de bases supportés par les méthodes d’extensions sur l’interface ISession, il suffit simplement de créer vos propres méthodes d’extension en transformant le type de votre choix vers un champ de byte.

public class HomeController : Controller
{
    [HttpGet]
    public IActionResult Index()
    {
        var userName = HttpContext.Session.GetString("userName");

        if(string.IsNullOrEmpty(userName))
        {
            userName = "Anonymous";
        }

        return Json(userName);
    }

    [HttpPost]
    public IActionResult Index(string userName)
    {
        HttpContext.Session.SetString("userName", userName);

        return Ok();
    }
}

Néanmoins, je ne vous conseille pas cette approche car elle n’est pas optimale en termes de testabilité. En effet, il est difficile d’assigner un un mock de ISession à votre contrôleur pour qu’il soit utilisable de cette façon.

Une autre approche que je vous conseille grandement, d’autant plus qu’à mon sens elle est bien plus esthétique en termes de code, consiste à injecter une instance de ISession via le constructeur de votre contrôleur. Le code du contrôleur devient donc celui présenté dans l’exemple suivant.

public class HomeController : Controller
{
    private readonly ISession _session;

    public HomeController(ISession session)
    {
        _session = session;
    }

    [HttpGet]
    public IActionResult Index()
    {
        var userName = _session.GetString("userName");

        if(string.IsNullOrEmpty(userName))
        {
            userName = "Anonymous";
        }

        return Json(userName);
    }

    [HttpPost]
    public IActionResult Index(string userName)
    {
        _session.SetString("userName", userName);

        return Ok();
    }
}

Cependant, l’interface ISession n’est normalement pas inscrite dans le conteneur IoC par défaut. Nous allons donc l’enregistrer et la lier à une factory qui ira chercher l’instance à partir d’un HttpContext.

Inscrivez l’interface ISession dans le conteneur d’injection en l’associant à une factory qui manipule l’interface IHttpContextAccessor.

services.AddTransient(typeof(ISession), serviceProvider =>
{
    var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
    return httpContextAccessor.HttpContext.Session;
});

Un test unitaire pour vos sessions

Comme je l’ai indiqué en préambule de cet article, l’un des intérêts du gestionnaire de session d’ASP.NET Core est sa testabilité. Ainsi, avec l’usage présenté dans la section précédente et qui manipule l’interface ISession, le test unitaire suivant peut-être écrit.

public class HomeControllerTests
{
    [Fact]
    public void GetCorrectValueFromSession()
    {
        var session = new Mock<ISession>();
        byte[] bytes = Encoding.UTF8.GetBytes("leo");
        session.Setup(x => x.TryGetValue("userName", out bytes));

        var homeController = new HomeController(session.Object);

        var result = homeController.Index();
        Assert.NotNull(result);
        var jsonResult = result as JsonResult;
        Assert.NotNull(jsonResult);
        var name = jsonResult.Value as string;
        Assert.NotNull(name);
        Assert.Equal("leo", name);
    }
}

Passer à une session distribuée

Si vous souhaitez que les données en sessions soient persistées pendant une plus longue durée, ou si vous souhaitez partager ces données entres les différents serveurs front qui composent votre ferme de serveur, alors vous devriez songer à passer vers un mode de stockage distribué. Une approche possible consiste à remplacer le stockage en mémoire par un stockage basé sur une base de données SqlServer.

Tout d’abord, la première étape consiste à remplacer les dépendances de votre projet afin d’utiliser le paquet Microsoft.Extensions.Caching.SqlServer à la place du paquet Microsoft.Extensions.Caching.Memory.

"Microsoft.Extensions.Caching.SqlServer": "1.0.0-rc1-final",
"Microsoft.AspNet.Session": "1.0.0-rc1-final"

Les versions précédentes d’ASP.NET imposaient une structure en base complexe pour y stocker les informations de sessions. Avec ASP.NET Core, ce mécanisme de cache basé sur SQL Server est beaucoup plus léger et se résume à une seule table. De plus, son utilisation n’est pas forcément bridée aux sessions, vous pouvez tout à fait l’utiliser pour d’autres usages.

Pour créer la table en question, vous pouvez utiliser un utilitaire distribué par le paquet Microsoft.Extensions.Caching.SqlConfig.

dnu commands install Microsoft.Extensions.Caching.SqlConfig

Une fois la commande installée, vous pouvez la manipuler simplement en suivant le modèle suivant.

sqlservercache create <connectionstring> <schema> <table>

N’oubliez pas d’ajouter un fichier de configuration à votre application afin d’indiquer la chaîne de connexion que le gestionnaire de cache doit utiliser. Je ne l’ai pas documenté dans ce billet, mais vous devrez donc utiliser le ConfigurationBuilder afin de récupérer une implémentation de IConfiguration pour ensuite manipuler cette chaîne de connexion.

"Data": {
  "SessionConnection": {
    "ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=SessionTest;Trusted_Connection=True;"
  }
}

Pour information, le SQL à générer correspond à l’instruction suivante.

CREATE TABLE [dbo].[Sessions](
    Id nvarchar(900) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL,
    Value varbinary(MAX) NOT NULL, 
    ExpiresAtTime datetimeoffset NOT NULL,
    SlidingExpirationInSeconds bigint NULL,
    AbsoluteExpiration datetimeoffset NULL,
    CONSTRAINT pk_Id PRIMARY KEY (Id));
CREATE NONCLUSTERED INDEX Index_ExpiresAtTime ON [dbo].[Sessions](ExpiresAtTime);

Enfin, la dernière étape consiste simplement à remplacer l’enregistrement du gestionnaire de cache en mémoire par celui utilisant SqlServer. Cela se fait via la méthode AddSqlServerCache. Le POCO d’options manipulé par cette méthode me sert à indiquer la chaine de connexion et le couple schéma/table qui sera utilisé pour stocker mes infos de cache.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSqlServerCache(options =>
    {
        options.ConnectionString = Configuration["Data:SessionConnection:ConnectionString"];
        options.SchemaName = "dbo";
        options.TableName = "Sessions";
    });
    services.AddSession();

    services.AddMvc();
}

Il n’y a rien d’autre à changer, mon application va automatiquement utiliser le cache distribué à présent.

Conclusion

J’espère que ce billet vous aurez permis de découvrir l’usage du gestionnaire de sessions avec ASP.NET Core. Cela devrait normalement vous aider à migrer au mieux vos applications existantes.

Nombre de vue : 779

AJOUTER UN COMMENTAIRE