UWAGA: Ta odpowiedź mówi o Entity Framework DbContext
, ale ma zastosowanie do wszelkiego rodzaju implementacji Jednostek Pracy, takich jak LINQ do SQL DataContext
i NHibernate ISession
.
Zacznijmy od echa Iana: posiadanie singla DbContext
dla całej aplikacji jest złym pomysłem. Jedyną sytuacją, w której ma to sens, jest posiadanie aplikacji jednowątkowej i bazy danych używanej wyłącznie przez tę instancję pojedynczej aplikacji. Nie DbContext
jest bezpieczny dla wątków, a ponieważ DbContext
dane w pamięci podręcznej szybko się zestarzeją. Doprowadzi to do różnego rodzaju problemów, gdy wielu użytkowników / aplikacji pracuje jednocześnie nad tą bazą danych (co jest oczywiście bardzo częste). Ale oczekuję, że już to wiesz i po prostu chcesz wiedzieć, dlaczego nie po prostu wstrzyknąć nowej instancji (tj. Przejściowy styl życia) DbContext
każdemu, kto jej potrzebuje. (aby uzyskać więcej informacji o tym, dlaczego pojedynczy DbContext
- lub nawet w kontekście na wątek - jest zły, przeczytaj tę odpowiedź ).
Zacznę od stwierdzenia, że rejestracja DbContext
jako przejściowa może działać, ale zazwyczaj chcesz mieć jedno wystąpienie takiej jednostki pracy w określonym zakresie. W aplikacji internetowej może być praktyczne zdefiniowanie takiego zakresu na granicach żądania sieciowego; w ten sposób styl życia na żądanie internetowe. Umożliwia to działanie całego zestawu obiektów w tym samym kontekście. Innymi słowy, działają w ramach tej samej transakcji biznesowej.
Jeśli nie masz celu, aby zestaw operacji działał w tym samym kontekście, w takim przypadku przejściowy styl życia jest w porządku, ale jest kilka rzeczy do obejrzenia:
- Ponieważ każdy obiekt ma swoją własną instancję, każda klasa, która zmienia stan systemu, musi wywołać
_context.SaveChanges()
(w przeciwnym razie zmiany zostaną utracone). Może to skomplikować Twój kod i nałożyć na niego drugą odpowiedzialność (odpowiedzialność za kontrolowanie kontekstu), co stanowi naruszenie zasady pojedynczej odpowiedzialności .
- Musisz upewnić się, że jednostki [ładowane i zapisywane przez
DbContext
] nigdy nie opuszczają zakresu takiej klasy, ponieważ nie można ich użyć w kontekście innej klasy. Może to ogromnie skomplikować Twój kod, ponieważ gdy potrzebujesz tych jednostek, musisz załadować je ponownie według identyfikatora, co może również powodować problemy z wydajnością.
- Ponieważ
DbContext
implementuje IDisposable
, prawdopodobnie nadal chcesz usunąć wszystkie utworzone instancje. Jeśli chcesz to zrobić, zasadniczo masz dwie opcje. Musisz je zutylizować w ten sam sposób zaraz po wywołaniu context.SaveChanges()
, ale w takim przypadku logika biznesowa przejmuje własność obiektu, który jest przekazywany z zewnątrz. Drugą opcją jest usunięcie wszystkich utworzonych instancji na granicy żądania HTTP, ale w takim przypadku nadal potrzebujesz pewnego zakresu, aby poinformować kontener, kiedy te instancje muszą zostać usunięte.
Inną opcją jest wcale nie wstrzykiwanie DbContext
. Zamiast tego wstrzykujesz plik, DbContextFactory
który jest w stanie utworzyć nową instancję (kiedyś używałem tego podejścia). W ten sposób logika biznesowa wyraźnie kontroluje kontekst. Jeśli mogłoby to wyglądać tak:
public void SomeOperation()
{
using (var context = this.contextFactory.CreateNew())
{
var entities = this.otherDependency.Operate(
context, "some value");
context.Entities.InsertOnSubmit(entities);
context.SaveChanges();
}
}
Zaletą tego jest to, że zarządzasz życiem DbContext
jawnie i łatwo to skonfigurować. Pozwala także na użycie jednego kontekstu w pewnym zakresie, który ma wyraźne zalety, takie jak uruchamianie kodu w pojedynczej transakcji biznesowej i możliwość przekazywania jednostek, ponieważ pochodzą one z tego samego DbContext
.
Minusem jest to, że będziesz musiał przechodzić DbContext
od metody do metody (która nazywa się Method Injection). Zauważ, że w pewnym sensie to rozwiązanie jest takie samo jak podejście „zakresowe”, ale teraz zakres jest kontrolowany w samym kodzie aplikacji (i być może jest wielokrotnie powtarzany). Jest to aplikacja odpowiedzialna za tworzenie i usuwanie jednostki pracy. Ponieważ DbContext
jest tworzony po skonstruowaniu wykresu zależności, konstruktor Wtrysk jest poza obrazem i musisz przejść do metody Wtrysk, gdy musisz przekazać kontekst z jednej klasy do drugiej.
Metoda wstrzykiwania nie jest taka zła, ale kiedy logika biznesowa staje się bardziej złożona i angażuje się więcej klas, będziesz musiał przekazać ją od metody do metody i klasy do klasy, co może bardzo skomplikować kod (widziałem to w przeszłości). W przypadku prostej aplikacji to podejście wystarczy.
Ze względu na wady to podejście fabryczne ma zastosowanie w przypadku większych systemów, inne podejście może być przydatne i to jest to, w którym pozwalasz kontenerowi lub kodowi infrastruktury / rootowaniu kompozycji zarządzać jednostką pracy. To jest styl, którego dotyczy twoje pytanie.
Pozwalając kontenerowi i / lub infrastrukturze obsłużyć to, kod aplikacji nie jest zanieczyszczony przez konieczność utworzenia (opcjonalnie) zatwierdzenia i usunięcia instancji UoW, co zapewnia logikę biznesową prostą i czystą (tylko jedna odpowiedzialność). Z tym podejściem wiążą się pewne trudności. Na przykład, czy popełniłeś i zlikwidowałeś instancję?
Pozbywanie się jednostki pracy można wykonać na końcu żądania internetowego. Jednak wielu ludzi błędnie zakłada, że jest to również miejsce, w którym można zatwierdzić jednostkę pracy. Jednak w tym momencie aplikacji po prostu nie można ustalić, czy jednostka pracy powinna zostać faktycznie zatwierdzona. np. jeśli kod warstwy biznesowej zgłosił wyjątek, który został złapany wyżej na stosie wywołań, zdecydowanie nie chcesz zatwierdzać.
Prawdziwym rozwiązaniem jest ponownie jawne zarządzanie jakimś zakresem, ale tym razem zrób to w katalogu głównym. Wyodrębnienie całej logiki biznesowej stojącej za wzorcem polecenia / procedury obsługi , będziesz mógł napisać dekorator, który można owinąć wokół każdego modułu obsługi poleceń, który pozwala to zrobić. Przykład:
class TransactionalCommandHandlerDecorator<TCommand>
: ICommandHandler<TCommand>
{
readonly DbContext context;
readonly ICommandHandler<TCommand> decorated;
public TransactionCommandHandlerDecorator(
DbContext context,
ICommandHandler<TCommand> decorated)
{
this.context = context;
this.decorated = decorated;
}
public void Handle(TCommand command)
{
this.decorated.Handle(command);
context.SaveChanges();
}
}
Zapewnia to, że kod infrastruktury trzeba zapisać tylko raz. Każdy solidny pojemnik DI umożliwia skonfigurowanie takiego dekoratora, aby był owijany wokół wszystkich ICommandHandler<T>
implementacji w spójny sposób.