Architektura heksagonalna w .NET bez zbędnych interfejsów

Radosław Krajewski • 4 listopada 2026
Kod źródłowy z kolorowym podświetleniem, przypominający strukturę architektury heksagonalnej, wyświetlany na ekranie laptopa.

Spis treści

Kontroler zamówień zapisuje dane przez Entity Framework Core, a po chwili ta sama reguła biznesowa ma obsłużyć komunikat z kolejki Azure Service Bus. Architektura heksagonalna pozwala oddzielić tę regułę od obu sposobów komunikacji, o ile granica między aplikacją a infrastrukturą nie pozostanie tylko nazwą folderu.

Schemat architektury heksagonalnej: aplikacja połączona portami z adapterami, które obsługują zewnętrzne interfejsy.

Reguły aplikacji powinny przetrwać wymianę technologii i kanału wejścia

  • Sześciokąt to symbol wielu możliwych połączeń, a nie wymóg sześciu portów.
  • Port opisuje potrzebę lub operację aplikacji; adapter tłumaczy ją na konkretną technologię.
  • Kontroler i konsument kolejki mogą uruchamiać ten sam przypadek użycia.
  • Interfejs ma sens, gdy chroni granicę, którą da się wskazać, przetestować lub wymienić.
  • Testy jednostkowe i integracyjne sprawdzają różne rzeczy, więc sama liczba atrap nie świadczy o dobrej architekturze.

Sześciokąt nie wymaga sześciu portów

Na schemacie widać figurę, więc łatwo uznać, że liczba boków coś narzuca. Nie narzuca: sześciokąt jest umownym obrazem aplikacji, która może komunikować się z różnymi aktorami i systemami zewnętrznymi.

Alistair Cockburn opisał podejście Ports and Adapters w raporcie technicznym z 2005 roku. Chodziło o to, by aplikację można było uruchamiać z różnymi wejściami i rozwijać bez uzależniania jej reguł od bazy czy urządzenia. Port wyznacza sposób rozmowy z aplikacją, a adapter przekłada ten sposób na konkretny kanał lub technologię.

To rozróżnienie zmienia sposób czytania diagramu. Jeśli na szkicu jest osiem adapterów, nie oznacza to błędu, a jeśli jest jeden interfejs, sama jego obecność jeszcze nie dowodzi, że granica została dobrze zaprojektowana.

Kontroler ASP.NET Core i konsument kolejki mogą wywołać ten sam port

Wyobraź sobie składanie zamówienia: klient wysyła żądanie HTTP, a system magazynowy może później przekazać podobne polecenie przez kolejkę. Oba wejścia powinny uruchamiać ten sam przypadek użycia, zamiast powielać reguły w kontrolerze i obsłudze komunikatu.

W .NET kontroler ASP.NET Core może być adapterem wejściowym, a konsument Azure Service Bus drugim. Port wejściowy, na przykład IPlaceOrder, opisuje operację aplikacji. Wewnątrz przypadek użycia korzysta z portu wyjściowego, takiego jak IOrderRepository; adapter oparty na EF Core realizuje go dla bazy danych.

Element Rola w przykładzie Co można wymienić
Kontroler HTTP Odczytuje żądanie i wywołuje port wejściowy Może go zastąpić lub uzupełnić konsument kolejki
IPlaceOrder Opisuje operację „złóż zamówienie” Kontrakt pozostaje wspólny dla wejść
IOrderRepository Opisuje zapis lub odczyt potrzebny przypadkowi użycia Może mieć adapter EF Core albo testową implementację
Adapter EF Core Przekłada port repozytorium na operacje bazy Może zostać zastąpiony innym sposobem trwałego zapisu

Ten układ daje prosty sprawdzian projektu: gdy zmieniasz format HTTP albo brokera komunikatów, czy trzeba ruszać reguły składania zamówienia? Jeśli tak, adapter przepuszcza szczegóły transportu do środka. Wspólny przypadek użycia jest tu cenniejszy niż wspólny kontroler, bo drugi kanał może mieć inne ograniczenia dotyczące ponowień, autoryzacji i błędów.

Twórz port wtedy, gdy potrafisz wskazać chronioną granicę

Każde nowe repozytorium z interfejsem może wyglądać na krok ku porządkowi, ale nadmiar abstrakcji tworzy dodatkowe nazwy i przejścia, których nikt nie potrzebuje. Port dodawaj wtedy, gdy izoluje potrzebę aplikacji od konkretnej technologii albo pozwala niezależnie uruchomić przypadek użycia.

Praktyczna reguła jest krótka: jeśli implementacja ma realną szansę się zmienić, istnieje drugi kanał wejścia albo potrzebujesz testować przypadek użycia bez infrastruktury, port zwykle ma uzasadnienie. Gdy interfejs tylko odbija metodę jednej klasy, a obie rzeczy zmieniają się razem, zostaw prostszy kod i wróć do abstrakcji, kiedy pojawi się wyraźna granica.

Sytuacja Prawdopodobny skutek Co zrobić
Interfejs ma jedną implementację i kopiuje jej metody Więcej plików bez odseparowania decyzji Usuń warstwę pośrednią albo nadaj portowi kontrakt opisujący potrzebę aplikacji
Kontroler bezpośrednio wywołuje DbContext i rozstrzyga reguły zamówienia Zmiana wejścia wymaga przenoszenia logiki Przenieś decyzje biznesowe do przypadku użycia, a zapis za port wyjściowy
Test przypadku użycia wymaga uruchomienia SQL Server Test sprawdza naraz regułę i konfigurację infrastruktury Podstaw prostą implementację portu w teście jednostkowym, a zapis sprawdź osobno testem integracyjnym

Ta reguła chroni przed projektem, w którym każda klasa ma interfejs tylko dlatego, że „tak robi czysta architektura”. Interfejs jest użyteczny, kiedy pozwala podjąć niezależną decyzję; sam w sobie nie czyni kodu bardziej heksagonalnym.

Testuj reguły osobno, a połączenia z infrastrukturą razem

Łatwo wpaść w pułapkę: skoro porty ułatwiają podstawianie atrap, wszystkie testy zaczynają sprawdzać współpracę z atrapami. Wtedy test potwierdza umowę z własnym mockiem, ale może przeoczyć błąd mapowania encji, konfiguracji EF Core albo integracji z brokerem.

Reguły domenowe testuj bezpośrednio, a przypadki użycia z prostymi implementacjami portów. Testy integracyjne zostaw dla wybranych połączeń, które muszą działać w rzeczywistym zestawieniu. Microsoft Learn rozróżnia test jednostkowy, który izoluje mały fragment kodu, od integracyjnego, który sprawdza współpracę komponentów, często razem z infrastrukturą.

Przykład dla zamówienia: test jednostkowy sprawdza, czy przypadek użycia odrzuca niedozwoloną zmianę statusu; test adaptera weryfikuje zapis przez EF Core; test API przechodzi przez aplikację testową ASP.NET Core i sprawdza, czy żądanie dociera do właściwego zachowania. Nie trzeba uruchamiać bazy dla każdej kombinacji reguł, ale trzeba choć raz sprawdzić, czy adapter naprawdę zapisuje dane w oczekiwanym kształcie.

Uważam, że najlepszy zestaw testów nie maksymalizuje liczby atrap ani testów integracyjnych. Dzieli odpowiedzialność tak, by szybki test wykrył błąd reguły, a węższy test infrastruktury błąd połączenia.

Podobieństwo do Clean Architecture nie oznacza identycznego schematu

Nazwy potrafią zaciemnić sprawę. Microsoft Learn opisuje Hexagonal, Ports and Adapters, Onion oraz Clean Architecture jako nazwy podobnych układów, w których zależności infrastruktury kierują się do Application Core. To użyteczne przybliżenie, ale nie oznacza, że każdy projekt musi mieć taki sam podział na warstwy i projekty.

W architekturze heksagonalnej myślenie zaczyna się od aplikacji i jej portów: kto ją wywołuje, czego potrzebuje i jakie adaptery to realizują? W Clean Architecture często mocniej wybrzmiewa podział kodu na warstwy i zasada kierunku zależności. W praktyce oba podejścia mogą prowadzić do podobnego kodu .NET, więc nazwa folderu nie rozstrzyga, czy granica jest poprawna.

Gdyby zespół dopiero wydzielał moduł zamówień z dużej aplikacji, postawiłbym na porty nazwane językiem przypadku użycia, takie jak IPlaceOrder, i niewielką liczbę adapterów. Pełny zestaw projektów warstwowych wprowadzałbym dopiero wtedy, gdy odrębne zespoły, kanały lub infrastruktura rzeczywiście potrzebują tych granic. Struktura kodu powinna podążać za zmianami, które system już przechodzi, a nie za diagramem.

Zacznij od jednej reguły, którą chcesz ochronić

Nie trzeba przebudowywać całej aplikacji, by skorzystać z tego wzorca. Wybierz przypadek użycia, którego reguły dziś przeciekają do kontrolera lub kodu dostępu do danych, i wydziel tylko te porty, które pozwolą uruchomić go niezależnie.

Jeśli po zmianie kanału wejścia logika zamówienia pozostaje taka sama, a po podmianie bazy nie trzeba poprawiać jej reguł, granica spełnia swoje zadanie. Od takiego małego sprawdzianu zacząłbym każdą decyzję o szerszym wdrożeniu architektury portów i adapterów.

FAQ - Najczęstsze pytania

Oba adaptery wejściowe wywołują wspólny port, na przykład IPlaceOrder, który opisuje przypadek użycia. Dzięki temu reguły składania zamówienia pozostają w aplikacji, a szczegóły HTTP i brokera nie muszą ich powielać.

Port ma uzasadnienie, gdy oddziela potrzebę aplikacji od konkretnej technologii, pozwala obsłużyć drugi kanał wejścia albo uruchomić przypadek użycia bez infrastruktury. Jeśli interfejs tylko kopiuje metody jednej klasy, a obie zmieniają się razem, może dodawać pliki bez realnej korzyści.

Reguły domenowe i przypadki użycia testuj bezpośrednio, podstawiając prostą implementację portu, bez uruchamiania bazy dla każdej kombinacji. Osobny test integracyjny adaptera sprawdza, czy zapis przez EF Core działa z rzeczywistą konfiguracją i w oczekiwany sposób.

Oba podejścia mogą prowadzić do podobnego kodu, w którym zależności infrastruktury kierują się do rdzenia aplikacji. Architektura heksagonalna skupia uwagę na portach i adapterach, a Clean Architecture mocniej akcentuje podział na warstwy i kierunek zależności. Żadne z nich nie wymaga identycznego podziału kodu na projekty.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

architektura heksagonalna
asp.net core
entity framework core
testy integracyjne
Autor Radosław Krajewski
Radosław Krajewski
Nazywam się Radosław Krajewski i od 6 lat zgłębiam tajniki programowania .NET, chmury Azure oraz sztucznej inteligencji. Moja przygoda z tymi technologiami zaczęła się od fascynacji tym, jak złożone problemy można rozwiązywać za pomocą kodu i innowacyjnych narzędzi. Staram się przekazywać tę wiedzę w sposób zrozumiały, dzieląc się swoimi doświadczeniami i spostrzeżeniami na kursdotnet.pl. W moich artykułach skupiam się na praktycznych aspektach, porównuję różne rozwiązania i analizuję najnowsze trendy, aby dostarczyć Wam rzetelne i aktualne informacje, które pomogą Wam rozwijać się w tej dynamicznie zmieniającej się dziedzinie.

Udostępnij artykuł

Napisz komentarz