Session-Per-Request en NHibernate con Action Filters en ASP.NET MVC

Una de las buenas prácticas a tener en cuenta antes de trabajar con NHibernate es la siguiente: La sesión y transacción pertenecen a un mismo ciclo de vida. Con esto quiero decir que es importante abrir una transacción cada vez que trabajamos con una base de datos dentro de una aplicación, ya sea para realizar una consulta, como una actualización, eliminación, etc.

Dado que en el post anterior, donde comencé a montar una aplicación con NHibernate y MVC, realicé los ajustes básicos para poder instanciar una sesión y dejar una clase, Connection.cs, lista para su uso, ahora voy a mostrar una forma adaptada a la estructura de MVC para manejar tanto la sesión como la transacción.

Llegados a este punto necesitamos conocer un nuevo concepto: Action Filters.
Un action filter es un atributo que podemos asignar tanto a una acción de nuestro controlador como a un controlador entero para poder proporcionarle una funcionalidad adicional. Para crear un atributo de tipo Action Filter, debemos generar una clase y heredar de ActionFilterAttribute. Esta clase contiene los siguientes métodos, ejecutados en este orden:

  1. OnActionExecuting, se lanza antes de que la acción sea ejecutada.
  2. OnActionExecuted, es llamada después de que la acción finalice.
  3. OnResultExecuting, comienza antes de que el resultado sea devuelto a la página.
  4. OnResultExecuted, ocurre cuando ha terminado de ejecutar el resultado que generó la acción.

En este caso, Session-Per-Request se podría traducir en Session-Per-Action ya que, lo que realmente nos interesa, es tener una sesión disponible para una acción en concreto o para todas las acciones de un controlador. Además, me interesa que la sesión de NHibernate esté disponible ANTES de realizar cualquier operación dentro de la acción en cuestión, ya que si no es así lo más probable es que se lanzara una excepción al no tener un objeto session listo al que consultar. Por último, para que el ciclo de vida se complete correctamente, cuando la acción solicitada finalice, necesito que mi transacción sea completada y mi sesión finalice junto con el ciclo de vida de la petición. Teniendo presentes estas anotaciones, podríamos obtener lo siguiente:

using System.Web.Mvc;
using NHibernate.Context;

namespace MovieManager.Models
{
public class SessionPerRequest : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext1)
{
var session = MvcApplication.SessionFactory.OpenSession();
session.BeginTransaction();
CurrentSessionContext.Bind(session);
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
var session = CurrentSessionContext.Unbind(MvcApplication.SessionFactory);
if (session != null)
{
if (session.Transaction.IsActive)
{
try
{
session.Transaction.Commit();
}
catch
{
session.Transaction.Rollback();
}
}

session.Close();
}
}
}
}

Para este ejemplo, he sobrescrito los métodos OnActionExecuting OnResultExecuted . En el primero de ellos, inicializo la sesión de NHibernate y bindeo la misma en el contexto de la aplicación. Por el contrario, en el segundo método recupero la sesión, compruebo si tiene alguna transacción en curso, realizo el commit (Si ocurriera algún error durante el commit de la transacción actual, se realizaría un rollback para evitar problemas) y cierro la sesión.

En el archivo web.config, necesitamos declarar una propiedad para indicar el contexto en el que se enmarca la aplicación. Esta propiedad es current_session_context_class y , en este caso, podríamos declararlo como web, tal y como indico en el siguiente código:

<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider, NHibernate</property>
<property name="connection.connection_string">
Server=localhostsqlexpress;initial catalog=MovieManager;Integrated Security=true
</property>
<property name="dialect">NHibernate.Dialect.MsSql2000Dialect</property>
<property name="current_session_context_class">web</property>
<property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
<mapping assembly="MovieManager"/>
</session-factory>
</hibernate-configuration>

Por otro lado, podemos comprobar que hacemos referencia a MvcApplication.SessionFactory, lo cual significa que, para tener un mayor acceso a nuestra “Factoría de sesiones” he posicionado su declaración en el archivo Global.asax para poder acceder desde cualquier parte de mi aplicación.

using System.Web.Mvc;
using System.Web.Routing;
using NHibernate;
using NHibernate.Cfg;

namespace MovieManager
{
// Note: For instructions on enabling IIS6 or IIS7 classic mode,
// visit http://go.microsoft.com/?LinkId=9394801

public class MvcApplication : System.Web.HttpApplication
{
public static ISessionFactory SessionFactory = new Configuration().Configure().BuildSessionFactory();

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);

}

protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
}

Para ir finalizando, podemos ver la forma de utilizar el atributo SessionPerRequest en un controlador donde necesitaremos persistencia. Colocamos el mismo entre corchetes bien en la declaración del controlador o en cualquiera de las acciones.

using System.Web.Mvc;
using MovieManager.Models;

namespace MovieManager.Controllers
{
[SessionPerRequest]
public class HomeController : Controller
{
public ActionResult Index()
{
var movieRepository = new MovieRepository(MvcApplication.SessionFactory);
return View(movieRepository.ListMovies());
}

public ActionResult About()
{
return View();
}
}
}

En la acción Index, creamos un objeto MovieRepository, donde le pasamos como parámetro el objeto SessionFactory declarado en el fichero Global.asax. Llegados a este punto debemos tener en cuenta de que la sesión de NHibernate, necesaria para llamar a la base de datos y recuperar el listado de películas, ya ha sido inicializada justo antes de entrar en la acción gracias al atributo SessionPerRequest. Si echamos un vistazo al repositorio, vemos lo siguiente:

using System.Collections.Generic;
using MovieManager.Models.Objects;
using NHibernate;

namespace MovieManager.Models
{
public class MovieRepository
{
private readonly ISessionFactory _session;

public MovieRepository(ISessionFactory sessionFactory)
{
_session = sessionFactory;
}

public IList<Movie> ListMovies()
{
return _session.GetCurrentSession().CreateCriteria(typeof(Movie)).List<Movie>();
}
}
}

A través del constructor de la clase MovieRepository, estamos inyectando el objeto SessionFactory. De tal manera que podemos utilizar la misma factoría de sesiones en cualquiera de los métodos implementados en esta clase, que necesiten hacer una consulta, actualización, etc. Cuando todo haya terminado, todo quedará perfectamente finalizado gracias a Action Filters y Session-Per-Request/Action.

¡Saludos!

10 comentarios en “Session-Per-Request en NHibernate con Action Filters en ASP.NET MVC”

  1. Hola Gisela, buena la explicación de tu post, pero ne salieron algunas dudas:
    1.-Como haces para testearlo? pq al testear tus controladores , tienes que verificar que tenga declarado el atributo en cada uno.
    2.- Al tener declarado el atributo tienes que hacerle un testing al atributo(que cree una sesion) y testear el controlador en si.
    3.- No te parecere que la aplicación pierde “mantenibilidad”
    4.- Tambien me parece que esa manera la aplicacion se vuelve más acoplada
    Saludos desde Lima, Perú

  2. Buenos días a todos 🙂

    En primer lugar, gracias por vuestros comentarios.

    @José Fabricio Rojas te muestro un ejemplo de cómo podríamos testear si un controlador tiene declarado un atributo en concreto. En realidad es bastante simple:

    [TestMethod]
    public void HomeUtilizaSessionPerReQuest()
    {
    var homeController = new HomeController();
    var attributes = AttributeUsageAttribute.GetCustomAttributes(homeController.GetType());
    CollectionAssert.Contains(attributes, new SessionPerRequest());
    }

    El segundo punto que me comentas, me parece bastante interesante a la par que cierto 🙂 Efectivamente sería bastante recomendable testear el atributo en cuestión. Y realmente debería de ser así porque deberíamos asegurarnos de varias cosas, como si la sesión está abierta después de llamar a ese método, lo mismo con la transacción, qué pasa si se produce un error (si el commit se realiza o no). Es decir, creo que son puntos importantes que se podrían testear y descartar estas posibilidades a la hora de que nuestra aplicación falle.

    Sobre el mantenimiento de la aplicación, la verdad no sé decirte… Uno de los puntos desde los cuales se puede enfocar Session Per Request desde un Action Filters es que el programador tiene mayor control para decidir qué controles/acciones tienen disponible la posibilidad de usar NH si lo necesitan. Es posible que tengamos controladores o acciones que no necesiten persistencia, que estén realizando sus consultas a un webservice, que nada tiene que ver con NH o abriendo un archivo, etc. Quizás tenemos controladores que, aunque el costo de abrir una session sea mínimo… no es necesario hacerlo (No me hace falta utilizar unas deportivas si no voy a salir a correr…) No sé si me explico.

    Otra ventaja que veo es poder reutilizar estos filtros en otros proyectos/aplicaciones… Algo que también se podría hacer con HttpModule pero, en el caso de Global.asax es bastante posible que tengas otros métodos, inicializaciones en él que realmente no te permiten reutilizar en otro proyecto. (En MVC por defecto inicializamos las rutas necesarias para poder arrancar la aplicación en el Global.asax)

    Espero haber resuelto tus dudas 🙂

    ¡Saludos!

  3. Con el permiso de Gisela, le contesto a José:

    Una implementación posible, a la que estoy seguro de que Gisela llegará en breve, inyectaría el repositorio en el controller via el constructor (con la ayuda de Windsor y reemplazando la ControllerFactory de MVC).

    De esta manera, el controller se puede probar ya que se inyecta el repositorio, el cual, por supuesto, estará “mockeado” y, por lo tanto, no necesitara del session factory.

    Por otro lado, no creo que sea necesario probar el atributo puesto que su código es sumamente sencillo y menos aun el funcionamiento asociado al controller puesto que eso es responsabilidad del runtime de MVC.

    Por ultimo, con este diseño, creo que quedan atendidas las dudas que planteas en el punto 3 y 4.

    Un saludo

  4. Hola. Muy bueno el post. Pero me quedó una duda. Como hago para testear una action de un controller ? lo quise hacer y me dice que la session no tiene un contexto asociado.

    Gracias

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *