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.

  1. Klasa AuditingEventListener implementuje NHibernatowe interfejsy listenerów zdarzeń: IPreInsertEventListener oraz IPreUpdateEventListener. Korzysta ona wewnętrznie z obiektu typu PersistenceModelAuditor i deleguje do niego zadania związane z audytowaniem.
  2. PersistenceModelAuditor zapisuje dane audytowe do obiektu na podstawie IPizzaUserContext, który przyjmuje w konstruktorze.

Konfiguracja - jak było

Konfigurując NHibernate w IoC zastosowałem powszechne podejście, w którym:

  1. Najpierw tworzony jest obiekt Configuration.
  2. Korzysta z niego zarejestrowana w kontenerze IoC metoda fabrykująca ISessionFactory, która jest oczywiście singletonem.
  3. ISession jest tworzony przez ISessionFactory.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:

  1. Skonfigurowanie event listenerów po utworzeniu kontenera przez aplikację webową.
  2. Service Locator
  3. IContainer wstzyknięty przez Autofaca do klasy PersistenceModelAuditor.

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.

Opublikowano: