Wenn dein Projekt zum Chaos wird
Hast du jemals ein C#-Projekt geöffnet, das vor einem Jahr geschrieben wurde, und mit Entsetzen festgestellt, dass nichts geändert werden kann, ohne zu riskieren, alles zu zerstören? Geschäftslogik ist eng mit Entity Framework gekoppelt, ASP.NET-Controller enthalten Datenbankgeheimnisse und Komponententests werden zum Albtraum der Integration mit einer echten Datenbank. Kommt dir das bekannt vor?
Dies ist ein klassisches Symptom fehlender Architektur. Die gute Nachricht: Es gibt ein bewährtes Rezept – die Hexagonale Architektur (auch bekannt als Ports & Adapters). Dieses von Alistair Cockburn entwickelte Muster ist nicht nur ein modischer Begriff. Es ist ein Lebensretter für mittlere und große C#-Projekte. Lass uns aufschlüsseln, wie es funktioniert, und gleich etwas Code schreiben.
Das Wesen der Hexagonalen Architektur: Kern und Peripherie
Stell dir ein Sechseck vor. Im Zentrum befindet sich deine Anwendung mit reiner Geschäftslogik, geschrieben in reinem C# ohne externe Abhängigkeiten. An den Rändern befinden sich Ports (Schnittstellen), über die der Kern mit der Außenwelt kommuniziert. Adapter sind die konkreten Implementierungen: ASP.NET-Controller, EF Core-Repositorys, RabbitMQ-Clients.
Die Hauptregel: Der Kern weiß nichts über die Außenwelt. Er kennt nur seine Ports. Dies ermöglicht es dir, die Datenbank, Benutzeroberfläche oder Nachrichtenwarteschlangen zu ändern, ohne die Geschäftslogik zu modifizieren.
Warum braucht ein C#-Entwickler das?
- Testbarkeit: Geschäftslogik kann ohne Datenbank, HTTP oder Dateisystem getestet werden.
- Flexibilität: Möchtest du PostgreSQL durch MongoDB ersetzen? Ändere einfach den Adapter.
- Lesbarkeit: Der Code beginnt eher wie eine Anforderungsspezifikation auszusehen, anstatt wie ein Netz von Abhängigkeiten.
- SOLID-Konformität: Insbesondere das Dependency-Inversion-Prinzip.
Projektstruktur in C#
In der Hexagonalen Architektur werden typischerweise drei Hauptschichten unterschieden. Für C# ist es praktisch, diese als separate Projekte innerhalb einer Lösung zu organisieren:
MyApp.sln├── MyApp.Domain // Kern: Entitäten, Wertobjekte, Ports (Schnittstellen)├── MyApp.Application // Anwendungsfälle (Anwendungsdienste)├── MyApp.Infrastructure // Adapter: EF Core, externe APIs, Logging└── MyApp.Api // Eingabeadapter: ASP.NET Controller / Minimal APIHinweis: Domain und Application sind reine Klassenbibliotheken ohne Verweise auf Microsoft.AspNetCore oder EntityFramework. Nur System und System.Threading.Tasks.
Code schreiben: Ein echtes Beispiel
Nehmen wir an, wir bauen ein Bestellsystem. Wir müssen eine Bestellung erstellen und speichern.
Schritt 1. Kern (Domain)
Hier leben Entitäten und Ports – Schnittstellen, die definieren, wie der Kern mit der Welt kommuniziert.
// MyApp.Domain/Entities/Order.cspublic class Order{ public Guid Id { get; private set; } public string CustomerName { get; private set; } public decimal TotalAmount { get; private set; } public DateTime CreatedAt { get; private set; }
public Order(string customerName, decimal totalAmount) { Id = Guid.NewGuid(); CustomerName = customerName; TotalAmount = totalAmount; CreatedAt = DateTime.UtcNow; }}
// MyApp.Domain/Ports/IOrderRepository.cs (Ausgabeport)public interface IOrderRepository{ Task<Order> GetByIdAsync(Guid id); Task AddAsync(Order order);}
// MyApp.Domain/Ports/IOrderNotifier.cs (Ausgabeport – Benachrichtigungen)public interface IOrderNotifier{ Task NotifyOrderCreatedAsync(Order order);}Beachte: IOrderRepository und IOrderNotifier sind Ports. Der Kern definiert, was er möchte, aber nicht, wie es implementiert wird.