Fragment interfejsu IOrderRepository w C# z metodami opisującymi operacje domenowe.

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.

TL;DR

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.

C# - dobre podejście
// 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:

C# - złe podejście
// ❌ 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);
}
Uwaga

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.

PlaceOrderHandler używa IOrderRepository z domeny. EfOrderRepository w infrastrukturze implementuje ten interfejs i korzysta z EF Core oraz SQL Server. Domena nie zależy od infrastruktury.
Warstwa aplikacji używa kontraktu z domeny. Infrastruktura implementuje ten kontrakt i obsługuje bazę danych. Strzałki pokazują zależności w kodzie, a linia przerywana oznacza implementację interfejsu.

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.

C# - Unit of Work
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.