Unit of Work w EF Core - kiedy potrzebna jest transakcja?

Przemysław Kwiatkowski • 11 listopada 2026
Implementacja wzorca unit of work w EF Core. Popraw swoje umiejętności na antondevtips.com.

Spis treści

Po kliknięciu „Zapisz” zamówienie pojawia się w bazie, ale rezerwacja produktu już nie: jedna operacja się udała, druga zakończyła błędem. Unit of Work grupuje zmiany w jedną pracę nad danymi, choć to, czy wszystkie zostaną zapisane atomowo, zależy od granicy transakcji.

Implementacja wzorca unit of work w EF Core. Popraw swoje umiejętności na antondevtips.com.

O spójności danych decyduje granica zapisu

  • DbContext śledzi zmiany encji i zapisuje je przez SaveChanges.
  • Jedno SaveChanges obejmuje zmiany w transakcji, jeśli dostawca bazy ją obsługuje.
  • Wiele zapisów wymaga jawnej transakcji, jeśli wszystkie operacje mają się udać albo wycofać razem.
  • Dodatkowy wrapper wokół DbContext ma sens, gdy wnosi konkretną regułę lub granicę architektoniczną.

Unit of Work zbiera zmiany przed zapisem

W typowym przebiegu aplikacja pobiera dane, modyfikuje kilka obiektów i dopiero na końcu wysyła zmiany do bazy. Unit of Work pilnuje, co zmieniło się w trakcie tej pracy, i koordynuje zapis. Dzięki temu aktualizacja zamówienia i pozycji zamówienia nie musi oznaczać osobnego zapisu po każdej zmianie.

Martin Fowler opisuje ten wzorzec jako mechanizm śledzący obiekty dotknięte transakcją biznesową oraz koordynujący zapis i rozwiązywanie problemów współbieżności. W praktyce chodzi więc o coś więcej niż metodę Commit(): ważne są wspólny zakres śledzenia i decyzja, kiedy praca z danymi jest gotowa do utrwalenia.

W EF Core tę rolę pełni zwykle DbContext. Śledzi encje, wykrywa ich zmiany i zapisuje je przez SaveChanges albo SaveChangesAsync. Kontekst powinien żyć krótko, w granicach pojedynczej operacji aplikacyjnej. Trzymany zbyt długo gromadzi śledzone obiekty i zaciera granicę między jednym przypadkiem użycia a kolejnym.

Jedno SaveChanges obejmuje zmiany w jednej transakcji

Załóżmy, że obsługa zamówienia dodaje rekord zamówienia, pozycje i zmniejsza stan magazynowy. Jeśli te zmiany są śledzone przez ten sam kontekst i trafiają do jednego SaveChangesAsync, EF Core domyślnie zapisuje je w transakcji, o ile dostawca bazy obsługuje transakcje. Błąd jednej z operacji wycofuje cały ten zapis.

Inaczej wygląda przypadek, w którym kod wywołuje zapis po dodaniu zamówienia, a drugi raz po aktualizacji magazynu. Gdy pierwszy zapis się powiedzie, a drugi nie, pierwszy może już pozostać w bazie. Jeśli obie części muszą tworzyć jedną całość, potrzebna jest jawna transakcja obejmująca oba wywołania. Microsoft Learn zaznacza też, że jawne sterowanie transakcją wymaga uwagi, gdy aplikacja korzysta z automatycznych strategii ponawiania po błędach połączenia.

Granica bazy kończy się jednak na bazie. Jeśli ten sam przypadek użycia po zapisie wysyła żądanie do operatora płatności albo publikuje komunikat do zewnętrznego brokera, transakcja EF Core nie cofnie skutków tych działań. W takim przepływie trzeba osobno zaprojektować obsługę ponowień, idempotencję lub zapis zdarzenia do outboxa.

Jawna transakcja wymaga zgodności z ponawianiem połączeń

Włączenie retry dla chwilowych błędów połączenia zmienia sposób, w jaki należy uruchamiać własną transakcję. Strategia ponawiania musi móc powtórzyć cały blok operacji, a nie tylko ostatnie SaveChangesAsync. Gdy kod sam rozpoczyna transakcję poza takim blokiem, provider SQL Server może zgłosić wyjątek o nieobsługiwanej transakcji inicjowanej przez użytkownika.

Reguła jest prosta: jeśli jeden SaveChanges wystarcza, pozostaw transakcję EF Core; jeśli potrzeba kilku zapisów, a cały zestaw ma być powtarzalny po błędzie, uruchom cały blok transakcji przez strategię zwróconą przez Database.CreateExecutionStrategy(). Przy błędzie dokładnie podczas zatwierdzania wynik transakcji może być nieznany, dlatego operacje warto projektować tak, by ich ponowienie nie tworzyło duplikatów.

Dochodzi jeszcze szczegół specyficzny dla SQL Server. EF Core tworzy punkt zapisu przed kolejnym SaveChanges wewnątrz trwającej transakcji, ale nie robi tego, gdy połączenie ma włączone MARS. Wtedy po błędzie stan transakcji może być niepewny. To dobry powód, by nie włączać jawnych transakcji odruchowo do każdej metody serwisowej.

Osobny Unit of Work dodawaj tylko z konkretnym powodem

W projektach EF Core często powstaje interfejs IUnitOfWork, który opakowuje DbContext, oraz generyczne repozytoria opakowujące DbSet. Taki układ może wyglądać na bardziej uporządkowany, ale jeśli każda metoda tylko przekazuje wywołanie dalej, powstaje druga warstwa o tej samej odpowiedzialności. DbContext już pełni rolę Unit of Work i dostarcza mechanizm dostępu do kolekcji encji.

Własny interfejs ma uzasadnienie, gdy wyznacza granicę, której rzeczywiście potrzebuje aplikacja: na przykład port w architekturze heksagonalnej, który ma odseparować domenę od EF Core, albo metodę zapisującą zmiany i zdarzenia w jednym kontrolowanym przebiegu. Samo ukrycie SaveChanges pod nazwą Commit nie daje tej korzyści. Uważam, że dodatkowa abstrakcja powinna wyrażać decyzję architektoniczną, a nie powielać API kontekstu.

Sytuacja Prawdopodobny problem Co zrobić
Wszystkie zmiany trafiają do jednego SaveChanges Osobna transakcja może być zbędna Pozostaw domyślną transakcję EF Core, jeśli provider ją obsługuje
Występuje kilka SaveChanges, które muszą się udać razem Wcześniejszy zapis może zostać zatwierdzony mimo późniejszego błędu Obejmij zapisy jawną transakcją
Jawna transakcja działa z włączonym retry Strategia może nie umieć powtórzyć transakcji rozpoczętej poza jej zakresem Uruchom całą transakcję w bloku strategii wykonania
DbContext jest używany równolegle w kilku zadaniach Kontekst nie jest bezpieczny wątkowo Oczekuj na każde wywołanie asynchroniczne albo użyj osobnych kontekstów
Nowa warstwa tylko przekazuje wywołania do DbContext Abstrakcja powiela istniejące API Usuń ją albo dodaj jej odpowiedzialność, której kontekst nie realizuje

Granica transakcji powinna odpowiadać jednemu przypadkowi użycia

Gdy kilka zmian w bazie razem opisuje jedną decyzję biznesową, powinny należeć do jednego przebiegu Unit of Work. W EF Core najczęściej wystarczy krótko żyjący DbContext i pojedyncze SaveChangesAsync; dodatkowa transakcja jest potrzebna dopiero wtedy, gdy operacja obejmuje kilka zapisów. Od tej granicy zacznij, zanim dodasz kolejną warstwę abstrakcji.

FAQ - Najczęstsze pytania

Gdy zmiany śledzone przez ten sam DbContext trafiają do jednego SaveChangesAsync, EF Core domyślnie zapisuje je w transakcji, jeśli dostawca bazy obsługuje transakcje. Błąd jednej z operacji wycofuje wtedy cały zapis.

Jeśli oba zapisy muszą się udać albo wycofać razem, obejmij je jawną transakcją. Bez niej pierwszy zapis może pozostać w bazie, nawet gdy drugi zakończy się błędem.

Uruchom cały blok transakcji przez strategię zwróconą przez Database.CreateExecutionStrategy(), aby można było powtórzyć wszystkie operacje, a nie tylko ostatnie SaveChangesAsync. Projektuj ponawiane operacje tak, by nie tworzyły duplikatów, ponieważ po błędzie podczas zatwierdzania wynik może być nieznany.

Dodaj go, gdy wyznacza potrzebną granicę architektoniczną, na przykład oddziela domenę od EF Core albo koordynuje zapis zmian i zdarzeń. Jeśli każda metoda interfejsu tylko przekazuje wywołanie do DbContext, warstwa powiela jego API.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

ef core
dbcontext
transakcje
współbieżność
Autor Przemysław Kwiatkowski
Przemysław Kwiatkowski
Jestem Przemysław i od 15 lat zajmuję się programowaniem .NET, chmurą Azure oraz sztuczną inteligencją. Moja przygoda z tymi technologiami zaczęła się od fascynacji możliwościami, jakie dają, a z czasem przerodziła się w pasję do tworzenia rozwiązań, które realnie wpływają na pracę i życie ludzi. Na kursdotnet.pl staram się dzielić się swoją wiedzą w sposób przystępny, tłumacząc złożone zagadnienia i pomagając zrozumieć, jak te dynamicznie rozwijające się obszary IT mogą być wykorzystane w praktyce. Dokładam wszelkich starań, aby prezentowane przeze mnie materiały były rzetelne, aktualne i oparte na sprawdzonych źródłach, a także aby uporządkować wiedzę w sposób ułatwiający jej przyswojenie.

Udostępnij artykuł

Napisz komentarz