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:
W testach jest on mockowany, w aplikacji webowej jego implementacja to:
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 oraz IPreUpdateEventListener. Korzysta ona wewnętrznie z obiektu typu PersistenceModelAuditor i deleguje do niego zadania związane z audytowaniem.
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:
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 przez ISessionFactory.OpenSession().
W skrócie wyglądało to tak:
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:
Utworzenie konfiguracji i skonfigurowanie Autofaca wyglądało tak:
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ść:
Właśnie - był jeszcze PizzaServerContext:
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 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:
RegisterConfiguration wygląda tak:
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:
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.