Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 19 de julio de 2016
ASP.NET CoreContinuamos lamiéndonos las heridas provocadas por los sucesivos terremotos que han supuesto las prereleases y releases públicas de ASP.NET Core, con objeto de ponernos al día en los cambios y novedades que vamos encontrando.

Hoy vamos a hablar de las variables de sesión, un tema que ya tratamos por aquí hace bastante tiempo, pero que conforme evolucionaba el producto ha cambiado lo suficiente como para dedicarle un nuevo post.

Como sabemos, simplemente decir que las variables de sesión permiten mantener en el servidor datos arbitrarios relativos al estado de un visitante durante su uso del sitio web. Aunque no están demasiado bien vistas por temas de limitación de escalabilidad y por atentar contra la naturaleza stateless de la web, sin duda son un recurso bastante utilizado porque simplifican muchos escenarios frecuentes de necesidades de mantenimiento de estado entre peticiones.

Básicamente, este mecanismo consiste en añadir a todos los visitantes una cookie  con un token que los identifica de forma única, lo que permite en el lado servidor mantener valores en un “diccionario” persistente exclusivo para cada usuario. Lo habitual es que este diccionario tenga un tiempo de vida limitado, normalmente coincidente el tiempo que el visitante pasa en nuestro sistema web.

En ASP.NET “clásico”, este mecanismo venía integrado de serie en el monolítico framework; podíamos desactivarlo o simplemente no usarlo, pero su código seguía estando ahí, acompañando a cada petición. Ahora, gracias a la modularidad de ASP.NET Core, las variables de sesión sólo se incluirán en nuestra aplicación si así lo decidimos de forma expresa.

1. Instalación de paquetes Nuget necesarios

Para utilizarlas, lo primero que debemos hacer es instalar el paquete Nuget Microsoft.AspNetCore.Session. Con esto ya tendremos disponibles los middlewares y las funciones básicas que necesitamos.

Ahora tenemos que pensar dónde queremos que se almacenen los datos de sesión; esta decisión es importante porque determinará la velocidad con la que el servidor leerá o escribirá datos e impondrá determinadas limitaciones de escalabilidad:
  • Si decidimos guardarlas en la memoria del servidor obtendremos la máxima velocidad a la hora de acceder a los datos, pero perderemos la capacidad de que la aplicación escale y pase a ejecutarse en varios servidores, granjas o distintas instancias. Por tanto, es la opción recomendable para aplicaciones pequeñas que van a ejecutarse en un único nodo, y en las que la persistencia de la información no sería importante (por ejemplo, si el servidor web se reinicia).
     
  • Si decidimos guardarlas en un almacén independiente perderemos algo de rendimiento, pero la información podrá ser compartida por los distintos servidores que ejecuten la aplicación, por lo que escala mejor, o al menos llegará hasta donde sea capaz de escalar el almacenamiento elegido. Es la opción recomendable para aplicaciones grandes que van a ser servidas desde distintos nodos por necesidades de rendimiento o tolerancia a fallos.
El sistema de sesiones de ASP.NET Core almacena los datos apoyándose en el sistema de caching del framework, que se distribuye en un paquete Nuget aparte. De esta forma, sólo tendremos que preocuparnos por instalar el paquete correspondiente, configurarlo apropiadamente, y el sistema de sesiones se encargará del resto.

Si decidimos utilizar la memoria del servidor para almacenar el estado de sesión, debemos instalar el paquete Microsoft.Extensions.Caching.Memory. En cambio, si preferimos opciones más apropiadas para entornos distribuidos, el framework ofrece de serie soporte para caching en Redis y SQL Server, que respectivamente se distribuyen en los paquetes Microsoft.Extensions.Caching.Redis y Microsoft.Extensions.Caching.SqlServer. En general, deberías elegir Redis si necesitáis alto rendimiento porque en este aspecto es muy superior a la opción SQL Server.

2. Configuración de componentes

Una vez instalado el paquete del sistema de caché elegido, como suele ser habitual, el siguiente paso sería añadir los correspondientes servicios al inyector de dependencias:
public void ConfigureServices(IServiceCollection services)
{
   ...
   services.AddMemoryCache(); // Add in-memory caching services
   services.AddSession();     // Add session services
   ...
}
El extensor AddMemoryCache() que estamos usando en el código anterior es el utilizado cuando usamos caché en memoria. En caso de usar Redis deberíamos sustituirlo por  AddDistributedRedisCache(), o por  AddDistributedSqlServerCache() en caso de usar SQL Server.

Todos ellos admiten un parámetro mediante el cual podemos configurar sus opciones, como cadenas de conexión, aspectos de su funcionamiento, etc. Por ejemplo, el siguiente código muestra una posible configuración del caché in-memory mediante la cual indicamos que deben realizarse comprobaciones de caducidad de entradas en la caché cada cinco minutos:
services.AddMemoryCache(opt =>
{
    opt.ExpirationScanFrequency = TimeSpan.FromMinutes(5);
});
Tras añadir los servicios en el sistema de inyección de dependencias, ya sólo nos falta incluir el middleware SessionMiddleware en el pipeline de ASP.NET mediante una llamada al extensor UseSession() de IApplicationBuilder:
public void Configure(IApplicationBuilder app, 
                      IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...    
    app.UseSession(); // Include session middleware
    ...
}

Ilustración del pipeline de ASP.NET Core con SessionMiddleware insertado antes del framework MVC

Obviamente, es importante añadir el middleware de gestión de sesiones antes que MVC u otros frameworks de mayor nivel que requieran utilizar este mecanismo.
Como en el caso anterior, UseSession()admite parámetros que permiten configurar determinados aspectos de su funcionamiento, como podemos observar en el siguiente ejemplo:

app.UseSession(new SessionOptions() 
{
    CookieName = "MyAppSession",
    CookiePath = "/",
    IdleTimeout = TimeSpan.FromMinutes(30),
    CookieHttpOnly = true
});

3. Establecer, obtener y eliminar variables de sesión

En primer lugar, es importante tener en cuenta que los pasos anteriores son obligatorios si queremos utilizar variables de sesión. Cualquier intento de utilización de las mismas sin haber instalado y configurado previamente los componentes que las proporcionan provocarán la aparición de un error:

Excepción produciéndose al intentar acceder a una variable de sesión sin haber configurado el sistema

El acceso a las variables de sesión se realiza utilizando la propiedad HttpContext.Session, es decir, la clase Controller no proporciona ya una propiedad Session como en versiones anteriores de MVC, aunque obviamente siempre podríamos crearnos un atajo sobre la propia clase o una de sus antecesoras. Por cierto, fijaos qué limpio queda usando las nuevas expresiones lambda en miembros de función de C# 6:
public class HomeController: Controller
{
    public ISession Session => HttpContext.Session;

    public IActionResult Index()
    {
        var homeIndexVisits = 
               1 + Session.GetInt32("HomeIndexVisits").GetValueOrDefault();

        Session.SetInt32("HomeIndexVisits", homeIndexVisits);
        return Content($"You visited this page {homeIndexVisits} times");
    }
}
Los que habéis usado variables de sesión en versiones anteriores de ASP.NET, o incluso de ASP “clásico”, seguro que os han llamado la atención esos métodos GetInt32() y SetInt32() de la propiedad Session. Pues sí, esto otro cambio importante: hasta ahora podíamos introducir cualquier tipo de objeto en variables de sesión, y el abuso de esta capacidad ha generado muchísimos problemas. He llegado a ver incluso contextos de datos o conexiones a bases de datos mantenidas en memoria de esta forma, creando unos problemas terribles en tiempo de ejecución, y seguro que algunos os habréis encontrado incluso con cosas peores (aunque he de decir que no se me ocurre ninguna ;D)

Para evitar estas aberraciones, en ASP.NET Core sólo podremos guardar en variables de sesión objetos serializados, y esto se manifiesta en el tipo de dato que es ahora el diccionario Session: un array de bytes. Así se acabó el establecer o leer objetos completos directos como hemos hecho siempre, tendremos que serializarlos a un array de bytes para guardarlos el almacén de sesión, así como deserializarlos para poder acceder a ellos. De esta forma, simplificamos el uso de almacenamientos distintos a la memoria local para esta información, facilitando la escalabilidad y mejorando la fiabilidad.

Por tanto, la única fórmula “oficial” para obtener y almacenar datos de sesión pasan por usar los métodos Get() y Set(), que operan exclusivamente con un array de bytes. Sin embargo, el framework ofrece adicionalmente los extensores GetInt32(),  SetInt32(), GetString() y SetString() para simplificar la operación con esos tipos de datos tan frecuentes. Un ejemplo para verlos en acción:
public class UserController : Controller
{
    public ISession Session => HttpContext.Session;
    public IActionResult Get()
    {
        var name = Session.GetString("name");
        var age = Session.GetInt32("age");
        if (name == null || age == null)
        {
            return Content("No valid user data");
        }
        else
        {
            return Content($"User {name} is {age}");
        }
    }

    public IActionResult Set(string name, int age)
    {
        Session.SetString("name", name);
        Session.SetInt32("age", age);
        return Content("User data changed");
    }
}
Si por cualquier motivo necesitamos guardar objetos o grafos más complejos (ojo, que ya os digo que no es conveniente por la repercusión que puede tener en consumo de recursos), podríamos crear un serializador personalizado capaz de convertir el objeto a array de bytes y viceversa, o bien utilizar algo más simple como JSON y guardarlo y recuperarlo como cadena de texto.

Finalmente, para eliminar variables de sesión tenemos dos opciones. La más violenta, utilizando el método Clear(), elimina todas las variables de sesión asociadas al visitante actual (aunque mantiene la cookie); otra, más selectiva, es utilizar el método Remove(), para eliminar sólo aquella cuya clave suministremos.

Ah, y una última observación: fijaos que no hay nada similar al Session.Abandon() que teníamos en versiones anteriores de ASP.NET, así como tampoco tenemos disponibles eventos como Session_Start() y Session_End()que podíamos capturar en el Global.asax cuando comenzaban o finalizaban las sesiones de usuario (por cierto, ni parece que los vaya a haber).

Publicado en Variable Not Found.

Aún no hay comentarios, ¡sé el primero!