Chain of Responsibility w C# – middleware ASP.NET Core
Ostatni wzorzec z serii opartej na Anatomii wzorca — 8 elementach opisu GoF. Chain of Responsibility — wzorzec, którego używasz w każdej aplikacji ASP.NET Core, nawet jeśli nigdy nie napisałeś tej nazwy w kodzie.
1. Intent (Zamierzenie)
Unika powiązania nadawcy żądania z jego odbiorcą, dając więcej niż jednemu obiektowi szansę na obsłużenie żądania. Łączy obiekty odbierające w łańcuch i przekazuje żądanie wzdłuż niego, aż któryś z obiektów je obsłuży.
Krótko: nadawca żądania nie wie ani nie musi wiedzieć, który dokładnie obiekt je obsłuży — wysyła je na początek łańcucha, a łańcuch sam decyduje.
2. Also Known As (Inne nazwy)
GoF nie podaje popularnej alternatywnej nazwy — w praktyce wzorzec bywa nazywany po prostu “pipeline” albo “łańcuchem handlerów”, szczególnie w kontekście middleware.
3. Motivation (Motywacja)
GoF opisuje system pomocy kontekstowej w GUI: użytkownik naciska F1 na przycisku. Przycisk może mieć własny tekst pomocy — jeśli tak, wyświetla go. Jeśli nie, przekazuje żądanie do okna dialogowego, które go zawiera. Jeśli dialog też nie ma pomocy dla tego kontekstu, przekazuje dalej do aplikacji. Obiekt inicjujący żądanie (przycisk) nie wie i nie musi wiedzieć, który poziom finalnie je obsłuży.
4. Applicability (Kiedy stosować)
Stosuj Chain of Responsibility gdy:
- więcej niż jeden obiekt może obsłużyć żądanie, a konkretny handler nie jest znany z góry — powinien zostać ustalony automatycznie,
- chcesz wysłać żądanie do jednego z kilku obiektów, nie wskazując odbiorcy jawnie,
- zestaw obiektów mogących obsłużyć żądanie powinien dać się określić dynamicznie (dodać/usunąć handler bez zmiany reszty kodu).
Sygnał ostrzegawczy w kodzie: długi ciąg if / else if sprawdzający kolejne warunki i przekazujący sterowanie dalej, gdy dany warunek nie pasuje.
5. Structure (Struktura)
┌──────────────────┐
│ <<abstract>> │
│ Handler │
│──────────────────│
│ -next: Handler │
│ +SetNext(h) │
│ +Handle(request) │
└──────────────────┘
△
┌─────┼─────────────┐
┌──────────┐ ┌──────────┐ ┌──────────┐
│ HandlerA │ │ HandlerB │ │ HandlerC │
│──────────│ │──────────│ │──────────│
│+Handle() │ │+Handle() │ │+Handle() │
└──────────┘ └──────────┘ └──────────┘
Każdy ConcreteHandler albo obsługuje żądanie samodzielnie, albo przekazuje je do _next — kolejnego ogniwa łańcucha. Klient zna tylko pierwszy element łańcucha, nie wie nic o pozostałych.
6. Participants (Uczestnicy)
- Handler — wspólny interfejs/klasa bazowa z metodą
Handle()i referencją do następnego handlera. - ConcreteHandler — obsługuje żądania, za które odpowiada; jeśli nie może, przekazuje dalej.
- Client — inicjuje żądanie, wysyłając je do pierwszego handlera w łańcuchu.
7. Collaborations (Współpraca)
Gdy klient wysyła żądanie, jest ono przekazywane wzdłuż łańcucha, aż jakiś ConcreteHandler przejmie odpowiedzialność za jego obsługę.
Żądanie nie jest kopiowane do wszystkich handlerów naraz (to byłby Observer) — przechodzi sekwencyjnie, jeden handler na raz, aż ktoś je przejmie.
8. Consequences (Konsekwencje)
Plus: zmniejsza powiązanie — nadawca nie musi znać, który obiekt obsłuży żądanie. Plus: elastyczność w przydzielaniu odpowiedzialności — łańcuch można przebudować w runtime. Minus: obsługa nie jest gwarantowana — żądanie może dojść do końca łańcucha bez obsłużenia, jeśli nie zaprojektujesz tego świadomie.
Ten “minus” to realna pułapka w kodzie produkcyjnym — cichy brak obsługi bywa trudniejszy do wykrycia niż jawny wyjątek.
Chain of Responsibility w C# — od diagramu do kodu
Scenariusz
Akceptacja wydatków firmowych: kierownik akceptuje do 1000 zł, dyrektor do 10 000 zł, powyżej — zarząd.
Bez wzorca — ciąg if/else
public class AkceptacjaZly
{
public string Akceptuj(decimal kwota)
{
if (kwota <= 1000) return "Zaakceptowane przez kierownika";
else if (kwota <= 10000) return "Zaakceptowane przez dyrektora";
else return "Zaakceptowane przez zarząd";
}
}
Działa, dopóki reguły są proste. Dodanie nowego poziomu akceptacji (np. dział finansowy dla kwot specjalnych) albo zmiana kolejności sprawdzania wymaga edycji tej samej metody — i staje się nieczytelne przy więcej niż 3-4 poziomach.
Z wzorcem — łańcuch handlerów
public abstract class AkceptacjaHandler
{
private AkceptacjaHandler? _next;
public AkceptacjaHandler UstawNastepny(AkceptacjaHandler next)
{
_next = next;
return next; // pozwala łańcuchować: a.UstawNastepny(b).UstawNastepny(c)
}
public string Obsluz(decimal kwota)
=> MozeObsluzyc(kwota) ? Zaakceptuj(kwota) : _next?.Obsluz(kwota) ?? "Brak uprawnień do akceptacji";
protected abstract bool MozeObsluzyc(decimal kwota);
protected abstract string Zaakceptuj(decimal kwota);
}
public class KierownikHandler : AkceptacjaHandler
{
protected override bool MozeObsluzyc(decimal kwota) => kwota <= 1000;
protected override string Zaakceptuj(decimal kwota) => "Zaakceptowane przez kierownika";
}
public class DyrektorHandler : AkceptacjaHandler
{
protected override bool MozeObsluzyc(decimal kwota) => kwota <= 10000;
protected override string Zaakceptuj(decimal kwota) => "Zaakceptowane przez dyrektora";
}
public class ZarzadHandler : AkceptacjaHandler
{
protected override bool MozeObsluzyc(decimal kwota) => true; // ostatnie ogniwo — akceptuje zawsze
protected override string Zaakceptuj(decimal kwota) => "Zaakceptowane przez zarząd";
}
var kierownik = new KierownikHandler();
kierownik.UstawNastepny(new DyrektorHandler()).UstawNastepny(new ZarzadHandler());
Console.WriteLine(kierownik.Obsluz(500)); // "Zaakceptowane przez kierownika"
Console.WriteLine(kierownik.Obsluz(15000)); // "Zaakceptowane przez zarząd"
Dodanie nowego poziomu (np. DzialFinansowyHandler dla specjalnych kategorii wydatków) to nowa klasa i jedna linijka w łańcuchu — zero zmian w istniejących handlerach.
Chain of Responsibility, którego już używasz — middleware ASP.NET Core
app.Use(async (context, next) =>
{
Console.WriteLine("Logowanie żądania...");
await next(context); // przekazanie do kolejnego ogniwa łańcucha
});
app.UseAuthentication(); // kolejne ogniwo
app.UseAuthorization(); // i kolejne
app.MapControllers(); // ostatnie ogniwo — faktyczna obsługa żądania
Każdy middleware w ASP.NET Core to ConcreteHandler — decyduje, czy obsłużyć żądanie samodzielnie (np. zwrócić 401 przy braku autoryzacji, kończąc łańcuch), czy wywołać next() i przekazać dalej. To dokładnie ten sam wzorzec co AkceptacjaHandler powyżej, tylko z inną nazwą metody.
Pułapka: zapomniane wywołanie next() milczy zamiast krzyczeć
app.Use(async (context, next) =>
{
Console.WriteLine("Coś tam robię...");
// ❌ brak await next(context) — pipeline zatrzymuje się tutaj,
// wszystkie kolejne middleware (w tym Twój kontroler) NIGDY się nie wykonają
});
To dokładnie GoF-owy “minus” z Consequences: obsługa nie jest gwarantowana. W middleware ASP.NET Core brak wywołania next() nie rzuca wyjątku — po prostu cicho urywa pipeline. Efekt: żądanie wisi albo zwraca pustą odpowiedź, a debugowanie “dlaczego kontroler się nie wykonuje” bywa mylące, bo błąd nie jest w kontrolerze, tylko kilka warstw wcześniej.
Kiedy NIE stosować Chain of Responsibility
- Dokładnie jeden, zawsze ten sam handler — bezpośrednie wywołanie metody jest prostsze niż łańcuch o długości jeden.
- Wszyscy handlerzy muszą się wykonać niezależnie od tego, czy poprzedni “obsłużył” żądanie — to już nie jest Chain of Responsibility (gdzie pierwszy pasujący kończy łańcuch), tylko broadcast — czyli Observer.
- Kolejność handlerów nie ma znaczenia i wszystkie muszą zawsze zadziałać — zwykła lista wywoływana w pętli jest czytelniejsza niż łańcuch z warunkowym przekazywaniem dalej.
Podsumowanie
Chain of Responsibility przekazuje żądanie wzdłuż sekwencji handlerów, aż któryś z nich je przejmie — nadawca nie wie i nie musi wiedzieć, który to będzie. To nie abstrakcyjna teoria: middleware pipeline w ASP.NET Core jest zbudowany dokładnie w ten sposób. Największa pułapka to brak gwarancji obsługi — świadomie zaprojektuj ostatnie ogniwo łańcucha (jak ZarzadHandler powyżej) albo jawnie obsłuż przypadek, gdy nikt w łańcuchu nie podjął żądania.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →