Здравствуйте, уважаемые читатели!
В этой статье я хочу ответить на типичный вопрос разработчика, начинающего использовать 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 приложения.
Возможно в посте содержатся неточности и даже ошибки, в связи с чем я буду благодарен, если вы мне на них укажете.
А зачем ты сессию кладешь в HttpContext, она у тебя и так в контейнере есть? Ты сделал LifecycleType.OncePerRequest соответственно сессия финализуруется после реквеста.
ОтветитьУдалитьserviceContainer.AddService(x => x.Resolve().OpenSession(),LifecycleType.OncePerRequest);
как то так.
Это LinFu. OncePerRequest не имеет отношения к циклу обработки HTTP запроса, а будет выполнять переданную лямбду при каждом запросе ISession. От чего я собственно и пытался уйти.
ОтветитьУдалитьда. я ошибся. А может тогда стоит сделать класс - обвязку который будет хранить сессию в контексте, и отдавать ее, а он уже будет зарегистрирован в контейнере. И его можно будет инжектить. Хотя асболютно без разницы)
ОтветитьУдалитьВелосипед. Есть NHibernate.Context.WebSessionContext.
ОтветитьУдалитьЕсли делать совсем правильно, то методы типа OpenSession/CloseSession - должны быть у класса типа NhSessionManager, время жизни которого также контролируется контейнером.
ОтветитьУдалитьКроме этого, еще неплохо бы реализовать паттерн Transaction-Per-Request, чтобы доступ был транзакционным по-умолчанию.
Про транзакции согласен. Но это потом.
ОтветитьУдалитьСпасибо за WebSessionContext, посмотрю. Как разберусь - напишу сюда.