Konfiguracja NHibernate w Autofacu
Jak pisałem w poprzednim poście, celem moich ostatnich działań było oddzielenie Pizza.Mvc
od Pizza.Framework
. Po wydzieleniu modułu Pizza.Utils
, był już tylko jeden punkt łączący te dwa moduły - przekazywanie informacji o zalogowanym użytkowniku, do NHibernatowego event handlera, który przeprowadza audyt obiektu przy jego tworzeniu bądź aktualizacji w bazie. Pierwotnie zrobiłem to w niezbyt estestyczny sposób, teraz to poprawiłem i w sumie zastanawiam się, czemu od razu nie zrobiłem dobrze.
Co jest potrzebne w każdym przypadku
Czyli klasy, których połączenie jest celem zabawy.
Po pierwsze - źródło danych o kontekście zalogowanego użytkownika
jest interfejs IPizzaUserContext
:
1 public interface IPizzaUserContext
2 {
3 IPizzaPrincipal CurrentUser { get; }
4 }
W testach jest on mockowany, w aplikacji webowej jego implementacja to:
1 public class DefaultApplicationUserContext : IPizzaUserContext
2 {
3 public IPizzaPrincipal CurrentUser
4 {
5 get { return HttpContext.Current.User as IPizzaPrincipal; }
6 }
7 }
Rejestracja go odbywa sie w AutofacModule
w module Pizza.Mvc
po uruchomieniu aplikacji webowej.
Raczej nic tu odkrywczego nie ma.
Po drugie - mechanizm audytu w oparciu o możliwości NHibernate
Ogólnie to wymaga refaktoryzacji, więc nie będę opisywał szczegółowo ani wklejał kodu, ale idea korzystania z kontekstu użytkownika się nie zmieni.
- Klasa
AuditingEventListener
implementuje NHibernatowe interfejsy listenerów zdarzeń:IPreInsertEventListener
orazIPreUpdateEventListener
. Korzysta ona wewnętrznie z obiektu typuPersistenceModelAuditor
i deleguje do niego zadania związane z audytowaniem. PersistenceModelAuditor
zapisuje dane audytowe do obiektu na podstawieIPizzaUserContext
, który przyjmuje w konstruktorze.
Konfiguracja - jak było
Konfigurując NHibernate w IoC zastosowałem powszechne podejście, w którym:
- Najpierw tworzony jest obiekt
Configuration
. - Korzysta z niego zarejestrowana w kontenerze IoC metoda fabrykująca
ISessionFactory
, która jest oczywiście singletonem. ISession
jest tworzony przezISessionFactory.OpenSession()
.
W skrócie wyglądało to tak:
1 builder.RegisterType<PersistenceModelAuditor>().AsSelf();
2
3 var sessionFactory = configuration.BuildSessionFactory();
4 builder.RegisterInstance(sessionFactory).As<ISessionFactory>().SingleInstance();
5 builder.Register(c =>
6 {
7 var session = sessionFactory.OpenSession();
8 session.EnableFilter(SoftDeletableFilter.FilterName);
9
10 return session;
11 }).As<ISession>().InstancePerLifetimeScope();
A skąd się brało configuration
? Z fabryki, którą napisałem, aby móc tworzyć konfigurację NHibernate zarówno na podstawie całego Assembly
jak i ITypeSource
(np. do testów), oraz po to, aby móc uzyskać ten obiekt w celu wygenerowania bazy. Fabryka ta wyglądała tak:
1 public static class NhConfigurationFactory
2 {
3 public static Configuration BuildConfiguration(string connectionString, Assembly assembly)
4 {
5 var autoPersistenceModel = AutoMap.Assembly(assembly, new AutomappingConfiguration());
6 return BuildConfiguration(connectionString, autoPersistenceModel);
7 }
8
9 public static Configuration BuildConfiguration(string connectionString, ITypeSource typeSource)
10 {
11 var autoPersistenceModel = AutoMap.Source(typeSource, new AutomappingConfiguration());
12 return BuildConfiguration(connectionString, autoPersistenceModel);
13 }
14
15 private static Configuration BuildConfiguration(string connectionString, AutoPersistenceModel autoPersistenceModel)
16 {
17 Action<Configuration> config = c =>
18 {
19 c.EventListeners.PreUpdateEventListeners = new IPreUpdateEventListener[] { new AuditingEventListener() };
20 c.EventListeners.PreInsertEventListeners = new IPreInsertEventListener[] { new AuditingEventListener() };
21 c.EventListeners.PreDeleteEventListeners = new IPreDeleteEventListener[] { new SoftDeleteEventListener() };
22 };
23
24 IPersistenceConfigurer sqlConfiguration = MsSqlConfiguration.MsSql2012
25 .ConnectionString(connectionString)
26 .ShowSql();
27
28 var fullModel = autoPersistenceModel
29 .Conventions.AddFromAssemblyOf<AutomappingConfiguration>()
30 .AddFilter<SoftDeletableFilter>();
31
32 var configuration = Fluently.Configure()
33 .Database(sqlConfiguration)
34 .Mappings(m => m.AutoMappings.Add(fullModel))
35 .ExposeConfiguration(config)
36 .BuildConfiguration();
37
38 return configuration;
39 }
40 }
Utworzenie konfiguracji i skonfigurowanie Autofaca wyglądało tak:
1 public class AutofacModule : Autofac.Module
2 {
3 protected override void Load(ContainerBuilder builder)
4 {
5 string connectionString = ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString;
6
7 var configuration = NhConfigurationFactory.BuildConfiguration(connectionString, typeof(Customer).Assembly);
8
9 AutofacRegisterHelper.RegisterPersistenceStuffAndServices(builder, configuration, typeof(Customer).Assembly, Assembly.GetExecutingAssembly());
10 }
11 }
To podejście tak wryło mi się w mózg, że wręcz uznałem je kiedyś za jedyną możliwość. Jak widać, główny problem polega na tym, że definicja wszystkich ICośtamEventListener
jest częścią obiektu Configuration
, który jest tworzony poza kontenerem IoC. Ponadto, listenery wymagają zaś kontekstu użytkownika, którego implementacja trafia do kontenera później niż powstaje konfiguracja NHibernate.
Ale skąd się brał kontekst użytkownika po stronie backendowej?
Jak widać AuditingEventListener
nie przymuje PersistenceModelAuditor
w konstrukotrze (a jak wiemy, korzysta z niego), więc coś tu jest niezgodnie z IoC. Jak więc to działało? Ano tak, że AuditingEventListener
miał w sobie taką sprytną właściwość:
1 private static PersistenceModelAuditor Auditor
2 {
3 get
4 {
5 // TODO: this is not nice hack, find way to get access to AutofacContainer from NHibernate
6 var persistenceModelAuditor = PizzaServerContext.Current.Container.Resolve<PersistenceModelAuditor>();
7 return persistenceModelAuditor;
8 }
9 }
Właśnie - był jeszcze PizzaServerContext
:
1 public class PizzaServerContext
2 {
3 public static void Initialize(IContainer container)
4 {
5 var context = new PizzaServerContext(container);
6 Current = context;
7 }
8
9 public static PizzaServerContext Current { get; private set; }
10
11 private PizzaServerContext(IContainer container)
12 {
13 this.Container = container;
14 }
15
16 public IContainer Container { get; private set; }
17 }
Metoda Initialize
była wołana już po utworzeniu kontenera, zarówno w testach integracyjnych, jak i po starcie aplikacji webowej.
Widziałem takie podejście kiedyś w pracy i myślałem, że jest dobre. Dopiero teraz zrozumiałem, że to nie jest żaden “server context” tylko ładnie nazwany “service locator”. :P I, że to rodzi same problemy. Po pierwsze - bezsensowna zależność między modułami aplikacji, które powinny móc żyć niezależnie od siebie nawet w dwóch fizycznych warstwach, po drugie trzeba pamiętać o wywoływaniu jakiegoś PizzaServerContext.Initialize
.
Konfiguracja - po zmianach
Rozważałem kilka pomysłów na poprawienie tego:
- Skonfigurowanie event listenerów po utworzeniu kontenera przez aplikację webową.
- Service Locator
IContainer
wstzyknięty przez Autofaca do klasyPersistenceModelAuditor
.
Pierwszy nie ma sensu, a service locatora już przypadkiem miałem, a bezpośrednie korzystanie z kontenera IoC w klasie wygląda szalenie strasznie. Podumałem jeszcze chwilkę i doszedłem do wniosku, że głównym problemem jest fakt, iż… Configuration
jest poza kontenerem. Zacząłem się zastanawiać, czemu tak to zrobiłem, i nie umiałem podać innej przyczyny niż ta, że tak było w samplach. Nie umiałem też znaleźć żadnego argumentu, dla którego Configuration
nie mogłoby być normalnie zarejestrowane w Autofacu. Jak pomyślałem, tak też zrobiłem.
Najpierw podstawy - usunięcie PizzaServerContext
i zastąpienie właściwości AuditingEventListener.Auditor
normalnym constructor injection pola typu PersistenceModelAuditor
.
Następnie przepisanie metody rejestrucjącej elementy NHibernate
w kontenerze:
1 builder.RegisterType<PersistenceModelAuditor>().AsSelf();
2
3 builder.Register(cx => RegisterConfiguration(cx, connectionString, autoPersistenceModel))
4 .As<Configuration>().SingleInstance();
5
6 builder.Register(cx =>
7 {
8 var configuration = cx.Resolve<Configuration>();
9 var sessionFactory = configuration.BuildSessionFactory();
10 return sessionFactory;
11 }).As<ISessionFactory>().SingleInstance();
12
13 builder.Register(cx =>
14 {
15 var sessionFactory = cx.Resolve<ISessionFactory>();
16 var session = sessionFactory.OpenSession();
17 session.EnableFilter(SoftDeletableFilter.FilterName);
18
19 return session;
20 }).As<ISession>().InstancePerLifetimeScope();
RegisterConfiguration
wygląda tak:
1 private static Configuration RegisterConfiguration(IComponentContext cx, string connectionString, AutoPersistenceModel autoPersistenceModel)
2 {
3 var persistenceModelAuditor = cx.Resolve<PersistenceModelAuditor>();
4 var auditingEventListener = new AuditingEventListener(persistenceModelAuditor);
5
6 Action<Configuration> config = c =>
7 {
8 c.EventListeners.PreUpdateEventListeners = new IPreUpdateEventListener[] {auditingEventListener};
9 c.EventListeners.PreInsertEventListeners = new IPreInsertEventListener[] {auditingEventListener};
10 c.EventListeners.PreDeleteEventListeners = new IPreDeleteEventListener[] {new SoftDeleteEventListener()};
11 };
12
13 var sqlConfiguration = MsSqlConfiguration.MsSql2012
14 .ConnectionString(connectionString)
15 .ShowSql();
16
17 var fullModel = autoPersistenceModel
18 .Conventions.AddFromAssemblyOf<AutomappingConfiguration>()
19 .AddFilter<SoftDeletableFilter>();
20
21 var configuration = Fluently.Configure()
22 .Database(sqlConfiguration)
23 .Mappings(m => m.AutoMappings.Add(fullModel))
24 .ExposeConfiguration(config)
25 .BuildConfiguration();
26
27 return configuration;
28 }
Przy okazji przerobiłem API do rejestrowania na fluent, więc sam proces rejestracji NHibernate w aplikacji wygląda moim zdaniem ładniej. Przykładowy kod w module Autofaca:
1 string connectionString = ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString;
2
3 builder.RegisterPersistence(connectionString, typeof(Customer).Assembly)
4 .RegisterApplicationServices(this.ThisAssembly);
Ponieważ Configuration
można teraz bezproblemowo wyciągnać z kontenera, to i NhConfigurationFactory
stała się zbędna.
Wnioski
Pełen sukces! Pozbyłem się dziwnych tworów (PizzaServerContext
, NhConfigurationFactory
), jest jedno źródło wszystkich obiektów (kontener Autofaca), nie muszę pamiętać o inicjalizowaniu jakiegoś szemranego service locatora, no i co najważniejsze pozbyłem się międzymodułowej zależności, która nie miała sensu.