пятница, 22 апреля 2011 г.

Управление сессиями NHibernate в приложениях ASP.NET MVC

Progg it

Здравствуйте, уважаемые читатели!

В этой статье я хочу ответить на типичный вопрос разработчика, начинающего использовать NHibernate в web-приложениях, разрабатываемых на основе ASP.NET MVC - как управлять сессиями и конфигурацией NHibernate в рамках веб-приложения. Это первая проблема, которая встречается разработчику, и для того, чтобы не потерять производительность, и не получить странных трудновоспроизводимых ошибок необходимо корретно реализовать этот механизм. В сети я находил несколько разных версий, и в этой статье я приведу ту, которая показалась мне наиболее удобной.

Итак, сначала немного теории. Как говорит вся документация на NHibernate - создавать конфигурацию и фабрику сессий затратная по времени операция, в то время как создавать сессию операция относительно быстрая. Таким образом, необходимо, чтобы в нашем приложении, конфигурация создавалась как можно реже, и была одна фабрика сессий, а сессии создавались для каждого HTTP запроса. Создавать больше одной сессии для HTTP запроса не имеет особого смысла.

Таким образом, самое подходящее место для конфигурирования и создания фабрики сессий - это обработчик Application_Start. Я использую DI-контейнер LinFu, но он может быть с легкостью заменен любым другим. Я думаю семантика выполняемых действий будет ясна из приведенного кода.

protected void Application_Start()
{
 AreaRegistration.RegisterAllAreas();

 var serviceContainer = new ServiceContainer();
 serviceContainer.AddService(CreateNhSessionFactory());
 ServiceContainerProvider.Init(serviceContainer);
 
 RegisterGlobalFilters(GlobalFilters.Filters);
 RegisterRoutes(RouteTable.Routes);
}

protected ISessionFactory CreateNhSessionFactory()
{
 var sessionFactory = Fluently.Configure()
  .Database(
   MsSqlConfiguration.MsSql2008.ConnectionString(
    x => x.FromConnectionStringWithKey("ApplicationServices"))
  )
  .Mappings(x => x.FluentMappings.AddFromAssemblyOf<Issue>())
  .BuildSessionFactory();
 return sessionFactory;
}

Думаю из кода видно, что в Application_Start конфигурируется NHibernate, создается фабрика сессий и помещается в DI контейнер. По умолчанию LinFu использует поведение типа Singleton (единственный объект на все приложение), если при регистрации сервиса передается конкретный объект. Итак, я добился того, что у меня будет одна фабрика сессий для всего ASP.NET MVC приложения. Замечу, что приложение ASP.NET - это отдельная тема для обсуждения, но как минимум следует знать, что в одном приложении могут обрабатываться тысячи запросов, создает и уничтожает приложение IIS в соответствии с настройками. Теперь нужно сделать так, чтобы у нас на один запрос была только одна сессия, которая будет использоваться всеми классами слоя доступа к данным.

На самом деле нет ничего проще. Достаточно вспомнить, что есть структура данных привязанная к конктретному запросу - HttpContext. Ей и воспользуемся, для хранения сессии в рамках обработки одного HTTP запроса:

protected const string NH_SESSION_KEY = "NH_REQUEST_SESSION";

protected ISession GetSession()
{
 if (HttpContext.Current.Items.Contains(NH_SESSION_KEY))
  return HttpContext.Current.Items[NH_SESSION_KEY] as ISession;
 var session = ServiceContainerProvider.Container
  .GetService<ISessionFactory>()
  .OpenSession();
 HttpContext.Current.Items.Add(NH_SESSION_KEY, session);
 return session;
}

protected void CloseSession()
{
 if (!HttpContext.Current.Items.Contains(NH_SESSION_KEY)) return;

 var session = HttpContext.Current.Items[NH_SESSION_KEY] as ISession;
 session.Flush();
 session.Close();
 session.Dispose();
}

Итак, мы реализовали метод, который возвращает мне объект сессии привязанный к текущему потоку обработки HTTP запроса. При этом создание сессии ленивое, она не будет создаваться и открываться когда этого не требуется. Осталось только зарегистрировать способ получения сессии в DI-контейнере. Нет ничего проще:

protected void Application_Start()
{
 AreaRegistration.RegisterAllAreas();

 RegisterServices();
 RegisterGlobalFilters(GlobalFilters.Filters);
 RegisterRoutes(RouteTable.Routes);
}

protected void RegisterServices()
{
 var serviceContainer = new ServiceContainer();

 serviceContainer.AddService(CreateNhSessionFactory());
 serviceContainer.AddService(x => GetSession(), LifecycleType.OncePerRequest);

 ServiceContainerProvider.Init(serviceContainer);
}

Параметр определяющий время жизни объекта в данном случае не имеет особого значения, так как требуемый нам способ контроля за временем жизни реализован в методе GetSession(). Остался последний штрих, добавить закрытие сессии после окончания обработки HTTP-запроса:

protected void Application_EndRequest()
{
 CloseSession();
}

Все. Приведенные фрагменты легко адаптируются для другого DI контейнера, для ASP.NET WebForms приложения.

Возможно в посте содержатся неточности и даже ошибки, в связи с чем я буду благодарен, если вы мне на них укажете.