Specification Pattern w C# — jak pisać czysty kod i nigdy więcej nie szukać buga w 20 miejscach
Piszesz
.Where(klient => klient.Wydal > 1000 && klient.KontoAktywne == true)
w serwisie. Działa. Kopiujesz to do drugiego serwisu. Też działa.
Po pół roku masz ten sam warunek rozsiany w 20 plikach — i właśnie przyszedł szef ze zmianą progu z 1000 na 2000.
Zmieniasz ręcznie w 20 miejscach, o dwóch zapominasz i masz buga na produkcji.
Brzmi znajomo?
W tym artykule nauczysz się Specification Pattern
Pierwszego wzorca architektonicznego, który rozwiązuje ten problem raz na zawsze. Dowiesz się, jak działa pod maską Expression<Func<T, bool>>, jak zbudować elastyczne, kompozytowe zapytania bez eksplozji klas oraz jak wdrożyć standard branżowy używany w komercyjnych projektach .NET.
Spis treści
- 1. Problem: copy-paste LINQ niszczy Twoją architekturę
- 2. Czym jest Specification Pattern?
- 3. Klasa bazowa Specification<T> — rozkładamy na atomy
- 4. Konkretna specyfikacja krok po kroku
- 5. Repozytorium jako strażnik architektury
- 6. Klocki Lego — kompozycja specyfikacji
- 7. Ardalis.Specification — standard produkcyjny
- 8. Checklista i podsumowanie
Problem: copy-paste LINQ niszczy Twoją architekturę

Wyobraź sobie taki scenariusz. Szef prosi o wyświetlenie “Wartościowych Klientów” na stronie głównej. Piszesz szybko
// HomeController.cs
var clients = await _context.Clients
.Where(k => k.Wydal > 1000 && k.KontoAktywne == true)
.ToListAsync();
Działa. Miesiąc później budujesz moduł newslettera:
// NewsletterService.cs
var clients = await _context.Clients
.Where(k => k.Wydal > 1000 && k.KontoAktywne == true) // copy-paste
.ToListAsync();
Pół roku i 20 plików później szef mówi: “Wartościowy klient to teraz taki, co wydał 2000, nie 1000.”
Musisz ręcznie przeszukać cały projekt. Zapomnisz o jednym miejscu, masz buga na produkcji.
Dlaczego to jest realny problem produkcyjny?
- – Zduplikowana logika biznesowa — jeden warunek żyje w wielu miejscach.
- – Brak nazwy — k.Wydal > 1000 && k.KontoAktywne == true nic nie mówi czytającemu kod za 6 miesięcy.
- – Brak testowalności — nie możesz przetestować tego warunku izolowanie bez odpytywania bazy.
- – Krucha zmiana — jedna aktualizacja wymaga znajomości całej codebazy.
💡 Zasada DRY (Don’t Repeat Yourself)
mówi wprost: logika biznesowa powinna mieć jedno, autorytatywne źródło prawdy. Specification Pattern jest odpowiedzią na tę zasadę w warstwie zapytań.
Zobacz też: [Zasada Single Responsibility w praktyce C#] | [Repository Pattern krok po kroku]
Czym jest Specification Pattern?

Specification Pattern (Wzorzec Specyfikacji) to wzorzec z książki Domain-Driven Design Erica Evansa. Idea jest prosta:
Zamiast pisać warunek w każdym miejscu, w którym pobierasz dane — zamknij go w osobnej klasie z nazwą.
Tę klasę możesz sobie wyobrazić jako pudełko z etykietą:
┌─────────────────────────────────────┐
│ WartosciowyKlientSpecyfikacja │ ← etykieta (nazwa klasy)
├─────────────────────────────────────┤
│ wydal > 1000 && aktywny == true │ ← reguła (logika)
└─────────────────────────────────────┘
Od teraz żaden serwis nie pisze .Where(…) ręcznie. Każdy po prostu bierze to pudełko i podaje je do repozytorium:
var clients = await _repository.ListAsync(new WartosciowyKlientSpec());
Szef zmienia progi? Zmieniasz kod w jednym jedynym miejscu — wewnątrz pudełka.
Co zyskujesz?
| Cecha | Bez Specification | Z Specification |
| Zmiana reguły | 20 miejsc w projekcie | 1 plik |
| Czytelność kodu | .Where(k => k.Wydal > 1000…) | new WartosciowyKlientSpec() |
| Unit testy | Wymaga bazy danych | Czysta klasa C#, zero zależności |
| Reużywalność | Copy-paste | Raz napisana, używana wszędzie |
Klasa bazowa Specification<T>— rozkładamy na atomy
Żeby wzorzec działał, potrzebujemy uniwersalnego kształtu pudełka — abstrakcyjnej klasy bazowej. Zaczniemy od jej najprostszej wersji:
// Specification.cs
public abstract class Specification<T>
{
// Przechowuje regułę LINQ jako DANE — nie wykonuje jej od razu!
public Expression<Func<T, bool>>? Criteria { get; }
protected Specification(Expression<Func<T, bool>>? criteria)
{
Criteria = criteria;
}
}
Co to za dziwny typ: Expression<Func<T, bool>>?

To jest kluczowy element całego wzorca. Warto go zrozumieć raz, a dobrze.
Func<T, bool> — to zwykły delegate (funkcja) w C#. Przyjmuje obiekt typu T i zwraca bool. Wykonuje się natychmiast w pamięci RAM. Nie nadaje się do bazy danych.
Expression<Func<T, bool>> — to Drzewo Wyrażeń (Expression Tree). To nie jest gotowy kod do wykonania — to dane opisujące intencję.
Sygnał dla kompilatora:
Nie wykonuj tej lambdy teraz. Zapamiętaj jej strukturę jako obiekt, żeby Entity Framework mógł ją odczytać i przetłumaczyć na zapytanie SQL.
Dokładnie dlatego nasza klasa bazowa tylko przechowuje tę instrukcję, nie robi nic więcej. Reguła czeka na wywołanie przez repozytorium.
// ❌ Func — wykonuje się w pamięci, nie trafi do SQL
Func<Order, bool> func = o => o.TotalAmount >= 1000;
// ✅ Expression — EF Core przetłumaczy to na WHERE w SQL
Expression<Func<Order, bool>> expr = o => o.TotalAmount >= 1000;
Konkretna specyfikacja krok po kroku
Mając klasę bazową, tworzymy nasze konkretne “pudełka“. Każda klasa dziedziczy po Specification<T> i przekazuje regułę do konstruktora bazowego:
public class PremiumActiveOrdersSpec : Specification<Order>
{
// Konstruktor przyjmuje próg kwotowy — specyfikacja jest parametryzowalna
public PremiumActiveOrdersSpec(decimal minAmount)
: base(o => o.TotalAmount >= minAmount && o.IsActive)
{
}
}
Co zyskujemy konkretnie?
1. Nazwa klasy = dokumentacja na żywo
// ❌ Co to znaczy?
.Where(o => o.TotalAmount >= 1000 && o.IsActive)
// ✅ Natychmiast wiadomo, co pobieramy
new PremiumActiveOrdersSpec(1000m)
2. Parametryzacja reguły
Próg możesz przekazać z konfiguracji lub z kontekstu użytkownika — bez zmiany logiki filtrowania.
3. Izolowana testowalność
// Unit test — zero Entity Frameworka, zero bazy danych
[Fact]
public void Should_Match_Active_Order_Above_Threshold()
{
var spec = new PremiumActiveOrdersSpec(minAmount: 500m);
var matchingOrder = new Order { TotalAmount = 800m, IsActive = true };
var tooLow = new Order { TotalAmount = 200m, IsActive = true };
var inactive = new Order { TotalAmount = 800m, IsActive = false };
Assert.True(spec.Criteria!.Compile()(matchingOrder));
Assert.False(spec.Criteria.Compile()(tooLow));
Assert.False(spec.Criteria.Compile()(inactive));
}
💡 Metoda .Compile() zamienia Expression z powrotem w Func — tylko na potrzeby testów jednostkowych. W produkcji EF Core nigdy jej nie wywołuje.
Repozytorium jako strażnik architektury
Mamy pudełko z regułą. Teraz potrzebujemy kogoś, kto potrafi je otworzyć i przekazać do bazy danych. To zadanie repozytorium.
// OrderRepository.cs
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public OrderRepository(AppDbContext context)
{
_context = context;
}
public async Task<IReadOnlyList<Order>> ListAsync(Specification<Order> spec)
{
var query = _context.Orders.AsQueryable();
if (spec.Criteria != null)
{
// Otwieramy pudełko i doklejamy regułę do EF Core
query = query.Where(spec.Criteria);
}
// Serwis nigdy nie zobaczy bazy danych — tylko gotową listę
return await query.ToListAsync();
}
}
Co tu naprawdę się dzieje?
Serwis biznesowy
│
│ "Chcę zamówienia premium"
│ new PremiumActiveOrdersSpec(1000m)
▼
Repozytorium
│ otwiera pudełko (spec.Criteria)
│ dokłada .Where() do IQueryable
▼
Entity Framework Core
│ tłumaczy Expression na SQL
▼
SQL Server
SELECT * FROM Orders
WHERE TotalAmount >= 1000 AND IsActive = 1
Serwis jest całkowicie odcięty od warstwy danych.
Nie wie, czy pod spodem jest SQL Server, PostgreSQL, SQLite czy plik JSON. To jest zero wycieków abstrakcji jedna z fundamentalnych zasad Clean Architecture.
Interfejs repozytorium
W projekcie produkcyjnym repozytorium implementuje interfejs, to pozwala podmienić implementację w testach:
// IOrderRepository.cs
public interface IOrderRepository
{
Task<IReadOnlyList<Order>> ListAsync(Specification<Order> spec);
Task<Order?> FirstOrDefaultAsync(Specification<Order> spec);
}
Klocki Lego — kompozycja specyfikacji

To jest moment, w którym Specification Pattern pokazuje swoją prawdziwą moc.
Wyobraź sobie, że szef prosi: Pokaż mi zamówienia premium, ale tylko te złożone dzisiaj.
Pułapka: Class Explosion
Początkujący programista tworzy:
PremiumActiveOrdersSpec
PremiumActiveOrdersFromTodaySpec // nowa klasa
PremiumActiveOrdersFromTodayForWarsawSpec // jeszcze jedna
PremiumActiveOrdersFromTodayForWarsawWithDiscountSpec // koszmar
To się nazywa Class Explosion — eksplozja klas. Zamiast czystej architektury dostajesz śmietnik.
Rozwiązanie: małe, niezależne klocki
Senior rozbija regułę na minimalne, ortogonalne specyfikacje:
// Klocek 1: tylko logika Premium
public class PremiumOrdersSpec : Specification<Order>
{
public PremiumOrdersSpec(decimal minAmount)
: base(o => o.TotalAmount >= minAmount) { }
}
// Klocek 2: tylko logika "z dzisiaj"
public class OrdersFromTodaySpec : Specification<Order>
{
public OrdersFromTodaySpec()
: base(o => o.OrderDate.Date == DateTime.Today) { }
}
Repozytorium przyjmujące wiele specyfikacji
Jedna drobna modyfikacja — słowo kluczowe params i repozytorium przyjmuje dowolną liczbę klocków:
// params pozwala przekazać 1, 2, 3... dowolnie wiele specyfikacji
public async Task<IReadOnlyList<Order>> ListAsync(params Specification<Order>[] specs)
{
var query = _context.Orders.AsQueryable();
foreach (var spec in specs)
{
if (spec.Criteria != null)
{
// Każdy klocek dokłada kolejny WHERE
query = query.Where(spec.Criteria);
}
}
return await query.ToListAsync();
}
Serwis biznesowy — tu dzieje się magia
// OrderService.cs
public async Task ProcessTodaysPremiumOrdersAsync()
{
// Budujemy zapytanie z klocków — jak Lego
var premiumSpec = new PremiumOrdersSpec(minAmount: 1000m);
var todaySpec = new OrdersFromTodaySpec();
// Przekazujemy oba klocki — repozytorium zrobi resztę
var orders = await _repository.ListAsync(premiumSpec, todaySpec);
// ... dalsza logika biznesowa
}
Wygenerowany SQL — sprawdź co EF Core wyśle do bazy:
SELECT *
FROM Orders
WHERE TotalAmount >= 1000
AND CONVERT(date, OrderDate) = '2026-03-22'
EF Core sam połączył dwa .Where() operatorem AND. Jedno zapytanie, zero nadmiarowego kodu.
Potęga kompozycji w liczbach
Mając 10 małych specyfikacji, możesz zbudować tysiące kombinacji zapytań SQL bez dopisywania ani jednej nowej linijki w repozytorium czy serwisie.
// Przykładowe kombinacje z tych samych klocków:
await _repository.ListAsync(premiumSpec);
await _repository.ListAsync(todaySpec);
await _repository.ListAsync(premiumSpec, todaySpec);
await _repository.ListAsync(premiumSpec, todaySpec, warsawSpec);
await _repository.ListAsync(todaySpec, discountedSpec);
// ... i tak dalej
Ardalis.Specification — standard produkcyjny
Implementacja, którą zbudowaliśmy powyżej, to doskonały fundament do nauki mechaniki wzorca. W projektach komercyjnych jednak nie piszemy jej od zera.
Standardem branżowym jest pakiet NuGet autorstwa Steva Ardalis Smitha jednego z głównych architektów ASP.NET Core Guidance.
Instalacja
dotnet add package Ardalis.Specification
dotnet add package Ardalis.Specification.EntityFrameworkCore
Co zyskujesz ponad naszą wersję?
| Funkcja | Nasza implementacja | Ardalis.Specification |
| Filtrowanie `.Where()` | ✅ | ✅ |
| Eager loading `.Include()` | ❌ | ✅ |
| Sortowanie `.OrderBy()` | ❌ | ✅ |
| Paginacja `.Skip().Take()` | ❌ | ✅ |
| Projekcje `.Select()` | ❌ | ✅ |
| Gotowe `SpecificationEvaluator` | ❌ | ✅ |
| Wsparcie EF Core | ❌ | ✅ |
Przykład z Ardalis.Specification
using Ardalis.Specification;
// Specyfikacja z Include i sortowaniem — niemożliwe w naszej wersji
public class PremiumOrdersWithItemsSpec : Specification<Order>
{
public PremiumOrdersWithItemsSpec(decimal minAmount)
{
Query
.Where(o => o.TotalAmount >= minAmount && o.IsActive)
.Include(o => o.Items) // eager loading powiązanych encji
.OrderByDescending(o => o.TotalAmount) // sortowanie
.Skip(0).Take(20); // paginacja
}
}
💡 Ardalis.Specification
to de facto standard w ekosystemie Clean Architecture dla .NET. Używają go projekty oparte na [eShopOnWeb] — oficjalnym przykładzie Microsoftu.
Checklista i podsumowanie
✅ Checklista: wdrożenie Specification Pattern
- – [ ] Utwórz klasę bazową Specification<T> z właściwością Expression<Func<T, bool>>? Criteria
- – [ ] Dla każdej reguły biznesowej stwórz osobną klasę dziedziczącą po Specification<T>
- – [ ] Nazwy klas kończ na Spec lub Specification (konwencja)
- – [ ] Zmodyfikuj repozytorium tak, by przyjmowało params Specification<T>[]
- – [ ] Napisz unit testy dla każdej specyfikacji (bez bazy danych!)
- – [ ] W projekcie produkcyjnym — zainstaluj Ardalis.Specification
- – [ ] Usuń gołe .Where() z warstwy serwisów i kontrolerów
Kluczowe pojęcia do zapamiętania
Expression<Func<T, bool>> — nie jest kodem do wykonania. To dane opisujące intencję lambdy, które EF Core potrafi przetłumaczyć na SQL.
Specification Pattern — wzorzec enkapsulujący logikę zapytania w named object. Jeden warunek biznesowy = jedna klasa = jedno miejsce zmiany.
Class Explosion — antywzorzec polegający na tworzeniu nowej klasy dla każdej kombinacji warunków. Rozwiązanie: małe, kompozytowe specyfikacje.
Repozytorium — strażnik architektury. Jedyne miejsce, które otwiera pudełko i przekazuje regułę do bazy danych.
Co dalej?
Specification Pattern to fundament. Gdy go opanujesz, naturalnymi kolejnymi krokami są:
- – CQRS — separacja zapytań od komend
- – MediatR — orchestracja logiki bez bezpośrednich zależności
- – Unit of Work — zarządzanie transakcjami ponad repozytoriami
- – Ardalis.Specification — pełna implementacja produkcyjna
🔗 Zobacz też
- C# Podstawy Programowania: Twój Pierwszy Krok w Świat Kodowania
- AI w .NET: Zostań Architektem Inteligentnych Aplikacji!
- C# Clean Architecture w Praktyce
- C# – Zbuduj Własnego Tetrisa! Kompletny Przewodnik
- 7 Dniowe Wyzwanie C# Tic Tac Toe
- C# Zbuduj Profesjonalny Portal Randkowy od Podstaw!
📣 Call To Action
👉 zostaw komentarz – co było dla Ciebie największym „aha momentem”
👉 udostępnij artykuł komuś, kto walczy z Func, Action
Zobacz także — powiązane artykuły
👉 MCP w .NET (C#) – jak zbudować serwer AI krok po kroku
👉 Tworzenie klas i obiektów w C# — kompletny przewodnik
👉 Pattern Matching w C# – switch expressions i type patterns
Dołącz do Listy VIP
I otrzymaj roadmapę Junior .NET Developer oraz najlepszą ofertę, gdy tylko ruszą zapisy!!!

