Здравствуйте, уважаемые читатели!
В этой статье я хочу ответить на типичный вопрос разработчика, начинающего использовать 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 приложения.
Возможно в посте содержатся неточности и даже ошибки, в связи с чем я буду благодарен, если вы мне на них укажете.