
Repository Pattern to jeden z najbardziej nadużywanych i źle rozumianych wzorców w całej branży. Przez ostatnie lata widziałem go w dziesiątkach projektów i w zdecydowanej większości przypadków był implementowany jako zwykły wrapper na ORM. To nie jest Repository Pattern - to przepisywanie API Entity Framework w cieńszej warstwie.
Repository nie jest wrapperem na ORM. Jego zadaniem jest oddzielenie domeny od
szczegółów persystencji. Jeśli Twoje repozytorium wycieka IQueryable<T>
lub przyjmuje wyrażenia LINQ jako argumenty - masz problem.
Czym naprawdę jest Repository
Według oryginalnej definicji Erica Evansa z książki Domain-Driven Design, Repository jest abstrakcją kolekcji obiektów domenowych. Klient kodu nie powinien wiedzieć skąd obiekty pochodzą - czy z bazy danych, pliku, czy z cache'a w pamięci. To jest sedno wzorca.
Interfejs repozytorium powinien być zdefiniowany w warstwie domeny i posługiwać się wyłącznie językiem domenowym. Nie ma tu miejsca na koncepcje infrastrukturalne.
// Warstwa domeny - czysta, bez zależności na EF
public interface IOrderRepository
{
Task<Order?> FindByIdAsync(OrderId id);
Task<IEnumerable<Order>> FindPendingForCustomerAsync(CustomerId customerId);
Task SaveAsync(Order order);
}
Czego unikać
Teraz porównaj to z tym, co najczęściej widzę w projektach:
// ❌ To nie jest Repository - to wrapper na DbSet
public interface IOrderRepository
{
IQueryable<Order> GetAll();
Task<Order?> GetByIdAsync(int id);
Task<IEnumerable<Order>> FindAsync(Expression<Func<Order, bool>> predicate);
Task AddAsync(Order entity);
Task UpdateAsync(Order entity);
Task DeleteAsync(int id);
}
Zwracanie IQueryable<T> z repozytorium to wyciek abstrakcji.
Warstwa wywołująca może dobudować dowolne zapytanie - w tym JOIN-y, GROUP BY,
paginację. Twoja "abstrakcja" nic nie ukrywa.
Problem Generic Repository
Drugi grzech główny to IRepository<T> - generyczne repozytorium
pasujące do wszystkich encji. Brzmi jak DRY, wygląda jak oszczędność czasu.
W praktyce prowadzi do jednego z dwóch scenariuszy:
- Interfejs jest tak ubogi, że i tak trzeba go rozszerzać dla każdej encji osobno.
- Interfejs jest tak bogaty, że przekształca się w kolejny wrapper na ORM, tyle że bardziej skomplikowany.
Repozytorium powinno być definiowane przez potrzeby domeny, nie przez strukturę bazy danych. Różne agregaty mają różne wzorce dostępu - wymuś to wprost w kontraktach.
Jak to naprawić
1. Konkretne metody zamiast filtrów
Każda metoda repozytorium powinna mieć nazwę, która komunikuje intencję domenową.
FindPendingForCustomerAsync mówi więcej niż
FindAsync(o => o.Status == "Pending" && o.CustomerId == id).
Pierwszy wariant jest testowalny za pomocą prostego in-memory fake'a.
Drugi wymaga albo prawdziwej bazy, albo mockowania wyrażeń LINQ.
2. Unit of Work - kiedy go użyć
Repository nie zarządza transakcjami. To zadanie IUnitOfWork.
Jeśli operacja wymaga zapisania kilku agregatów atomowo, koordynuje to
Unit of Work - nie repozytorium i nie serwis aplikacyjny bezpośrednio.
public class PlaceOrderHandler
{
private readonly IOrderRepository _orders;
private readonly IUnitOfWork _uow;
public async Task HandleAsync(PlaceOrderCommand cmd)
{
var order = Order.Place(cmd.CustomerId, cmd.Items);
await _orders.SaveAsync(order);
await _uow.CommitAsync();
}
}
Podsumowanie
Repository Pattern jest wartościowy - ale tylko gdy rozumiemy po co istnieje. Jego rola to izolacja domeny od infrastruktury, nie upraszczanie API ORM-a. Jeśli jutro zmienisz bazę danych z SQL Server na MongoDB, interfejsy repozytoriów nie powinny wymagać żadnej zmiany. Jeśli wymagają - coś poszło nie tak.
- Definiuj repozytoria w warstwie domeny, implementuj w infrastrukturze.
- Używaj metod z semantyką domenową, nie filtrów LINQ.
- Unikaj
IQueryable<T>w interfejsach repozytoriów. - Generic Repository to zło konieczne - unikaj, jeśli możesz.
- Transakcje należą do Unit of Work, nie repozytorium.