Zum Hauptinhalt springen
Alle Beiträge
Software Engineering6 Min. Lesezeit

Clean Architecture in .NET: Enterprise-Anwendungen für Wandel strukturieren

Clean Architecture in .NET umsetzen — Domänenschicht, Use Cases, Dependency Inversion und CQRS mit MediatR für Enterprise-Anwendungen.

Enterprise-Anwendungen leben Jahre, manchmal Jahrzehnte. Die Frameworks, auf denen sie aufbauen, entwickeln sich weiter, Geschäftsanforderungen ändern sich, und die Teams, die sie pflegen, wechseln. Clean Architecture ist die Antwort auf diese Realität: Strukturieren Sie Ihre Anwendung so, dass der wichtigste Code — Ihre Geschäftslogik — von den Details isoliert ist, die sich am häufigsten ändern.

Das ist keine akademische Übung. Wir haben Clean Architecture in .NET-Projekten mit 10 bis über 200 Entwicklern angewandt, und die hier beschriebenen Muster stammen aus der Praxis, nicht aus Lehrbüchern.

Das Kernprinzip: Dependency Inversion

Clean Architecture hat eine nicht verhandelbare Regel: Abhängigkeiten zeigen nach innen. Die Domänenschicht hängt von nichts ab. Die Anwendungsschicht hängt von der Domäne ab. Infrastruktur und Präsentation hängen von der Anwendungsschicht ab. Niemals umgekehrt.

Warum das in der Praxis entscheidend ist:

  • Ihre Domänenlogik kann ohne Datenbank, ohne HTTP-Server, ohne Azure getestet werden.
  • Sie können Ihr ORM, Ihren Message Broker oder Ihren Cloud-Provider austauschen, ohne Geschäftsregeln anzufassen.
  • Neue Entwickler verstehen die Domäne durch das Lesen reiner C#-Klassen, nicht durch Navigation durch Framework-Konfiguration.

Der Lackmustest: Können Sie Ihre Geschäftsregeln in einem Unit-Test ausführen, der in unter einer Sekunde startet und keine externen Ressourcen benötigt? Falls nicht, zeigen Ihre Abhängigkeiten in die falsche Richtung.

Die vier Schichten in der Praxis

Schicht 1: Domäne

Die Domänenschicht enthält Entities, Value Objects, Domain Events und Domain Services. Sie hat keinerlei Abhängigkeiten zu externen Paketen — kein EF Core, kein MediatR, kein System.Text.Json.

Was hierhin gehört:

  • Entities mit Verhalten, keine anämischen Datenhüllen. Eine Order-Entity sollte wissen, wie man AddLineItem, ApplyDiscount und CalculateTotal aufruft. Geschäftsregeln leben auf der Entity.
  • Value Objects für Konzepte mit wertebasierter Gleichheit: Money, Address, EmailAddress, DateRange.
  • Domain Events, die signifikante Geschäftsvorfälle erfassen: OrderPlaced, PaymentReceived, InventoryThresholdBreached.
  • Domain Services für Operationen, die nicht zu einer einzelnen Entity gehören: PricingService, InventoryAllocationService.

Was nicht hierhin gehört:

  • Repository-Interfaces (die gehören in die Anwendungsschicht)
  • DTOs oder API-Modelle
  • Alles mit den Wörtern „Handler", „Controller" oder „Middleware"
Csharp
// Domain Entity mit Verhalten — kein anämisches Modell
public class Order
{
    private readonly List<LineItem> _lineItems = new();
    public IReadOnlyCollection<LineItem> LineItems => _lineItems.AsReadOnly();
    public OrderStatus Status { get; private set; }

    public void AddLineItem(Product product, int quantity)
    {
        if (Status != OrderStatus.Draft)
            throw new DomainException("Eine abgesendete Bestellung kann nicht geändert werden.");

        var existing = _lineItems.FirstOrDefault(li => li.ProductId == product.Id);
        if (existing is not null)
            existing.IncreaseQuantity(quantity);
        else
            _lineItems.Add(new LineItem(product, quantity));
    }
}

Schicht 2: Anwendung (Use Cases)

Die Anwendungsschicht orchestriert Domänenobjekte zur Erfüllung von Use Cases. Sie definiert Commands, Queries, deren Handler und die Interfaces, die die Infrastruktur implementieren muss.

Was hierhin gehört:

  • Command- und Query-Definitionen: PlaceOrderCommand, GetOrderByIdQuery
  • Handler: Orchestrieren Domänenlogik, rufen Repository-Interfaces auf, publizieren Domain Events
  • Interface-Definitionen: IOrderRepository, IPaymentGateway, IEmailService
  • Anwendungsebenen-Validierung: Input-Validierung, die keine Domänenregel ist (z. B. „dieses Feld ist Pflicht")
  • Output-DTOs: Response-Modelle, die die Präsentationsschicht konsumiert

Wesentliche Design-Entscheidungen:

  • Ein Handler pro Use Case. Keine „OrderService"-Gottklassen mit 30 Methoden. Jeder Use Case ist eine eigene Klasse.
  • Handler sind schlank. Sie laden Domänenobjekte, rufen Domänenverhalten auf, persistieren Änderungen und publizieren Events. Enthält ein Handler Geschäftslogik, gehört diese in die Domäne.
  • Keine Framework-Abhängigkeiten. Die Anwendungsschicht darf weder ASP.NET Core noch EF Core oder Azure SDKs referenzieren. Sie referenziert Interfaces, die die Infrastruktur implementiert.

Schicht 3: Infrastruktur

Die Infrastrukturschicht liefert konkrete Implementierungen der in der Anwendungsschicht definierten Interfaces.

Was hierhin gehört:

  • Repository-Implementierungen mit EF Core, Dapper oder anderer Datenzugriffstechnologie
  • Externe Service-Integrationen: Payment-Gateways, E-Mail-Provider, Cloud-Speicher
  • Message-Broker-Produzenten und -Konsumenten: Azure Service Bus, RabbitMQ
  • Caching-Implementierungen: Redis, In-Memory-Cache
  • Dateisystemzugriff, HTTP-Clients und andere I/O-Operationen

Zentrale Regel: Infrastrukturklassen implementieren Interfaces der Anwendungsschicht. Sie exponieren niemals Framework-spezifische Typen an die Anwendungsschicht.

Csharp
// Infrastruktur implementiert das Interface der Anwendungsschicht
public class SqlOrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

    public SqlOrderRepository(AppDbContext context) => _context = context;

    public async Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct)
        => await _context.Orders
            .Include(o => o.LineItems)
            .FirstOrDefaultAsync(o => o.Id == id, ct);

    public async Task AddAsync(Order order, CancellationToken ct)
    {
        await _context.Orders.AddAsync(order, ct);
        await _context.SaveChangesAsync(ct);
    }
}

Schicht 4: Präsentation (API / UI)

Die Präsentationsschicht ist der Einstiegspunkt — ASP.NET-Core-Controller oder Minimal APIs, Blazor-Komponenten oder eine Konsolenanwendung. Sie ist die dünnste Schicht.

Verantwortlichkeiten:

  • HTTP-Request-/Response-Mapping
  • Authentifizierungs- und Autorisierungs-Middleware
  • Input-Deserialisierung und Output-Serialisierung
  • Swagger-/OpenAPI-Konfiguration
  • Dependency-Injection-Container-Setup (die Composition Root)

Die Composition Root ist der Ort, an dem alle Schichten zusammenkommen. Dies ist die einzige Stelle, die alle Projekte referenziert:

Csharp
// Program.cs — die Composition Root
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IPaymentGateway, StripePaymentGateway>();
builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssembly(typeof(PlaceOrderCommand).Assembly));

CQRS mit MediatR: Eine praktische Implementierung

MediatR ist nicht zwingend für Clean Architecture, passt aber hervorragend zur Implementierung von CQRS innerhalb der Anwendungsschicht.

Commands (Schreiboperationen)

Csharp
public record PlaceOrderCommand(
    Guid CustomerId,
    List<OrderItemDto> Items) : IRequest<OrderResult>;

public class PlaceOrderHandler : IRequestHandler<PlaceOrderCommand, OrderResult>
{
    private readonly IOrderRepository _orders;
    private readonly IUnitOfWork _unitOfWork;

    public async Task<OrderResult> Handle(
        PlaceOrderCommand request, CancellationToken ct)
    {
        var order = Order.Create(new CustomerId(request.CustomerId));
        foreach (var item in request.Items)
            order.AddLineItem(item.ProductId, item.Quantity);

        await _orders.AddAsync(order, ct);
        await _unitOfWork.CommitAsync(ct);

        return new OrderResult(order.Id, order.Status);
    }
}

Queries (Leseoperationen)

Csharp
public record GetOrderQuery(Guid OrderId) : IRequest<OrderDetailDto?>;

public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDetailDto?>
{
    private readonly IReadOnlyOrderRepository _orders;

    public async Task<OrderDetailDto?> Handle(
        GetOrderQuery request, CancellationToken ct)
        => await _orders.GetDetailByIdAsync(request.OrderId, ct);
}

Querschnittsbelange via Pipeline Behaviors

MediatR-Pipeline-Behaviors behandeln Belange, die mehrere Handler betreffen:

  • Validierung: Commands vor dem Handler mit FluentValidation validieren
  • Logging: Jeden Command und jede Query mit Ausführungszeit protokollieren
  • Transaktionsmanagement: Command-Handler in eine Datenbank-Transaktion einwickeln
  • Caching: Query-Ergebnisse mit konfigurierbarer Ablaufzeit cachen
Csharp
public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public async Task<TResponse> Handle(TRequest request,
        RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        var failures = _validators
            .Select(v => v.Validate(request))
            .SelectMany(r => r.Errors)
            .Where(f => f is not null)
            .ToList();

        if (failures.Count > 0)
            throw new ValidationException(failures);

        return await next();
    }
}

Testbarkeit: Die Rendite

Clean Architecture macht Testen unkompliziert, weil Abhängigkeiten injizierbar sind und Domänenlogik pur ist.

Teststrategie nach Schicht:

SchichtTesttypAbhängigkeitenGeschwindigkeit
DomäneUnit-TestsKeineMillisekunden
AnwendungUnit-TestsGemockte InterfacesMillisekunden
InfrastrukturIntegrationstestsEchte Datenbank (Testcontainers)Sekunden
PräsentationIntegrationstestsTestServer + echte PipelineSekunden

Die Schlüsselmetrik: 80 % Ihrer Geschäftslogik-Tests sollten schnelle Unit-Tests ohne externe Abhängigkeiten sein. Wenn die Mehrheit Ihrer Tests eine laufende Datenbank benötigt, hat Ihre Architektur ein Dependency-Inversion-Problem.

Häufige Fehler, die wir beobachten

  • Anämisches Domänenmodell: Entities als reine Datencontainer, gesamte Logik in Handlern. Das untergräbt den gesamten Zweck der Domänenschicht.
  • Durchsickern von Infrastrukturtypen: EF Core DbSet<T> über Repository-Interfaces exponiert. Die Anwendungsschicht sollte nicht wissen, welches ORM verwendet wird.
  • Over-Engineering für kleine Projekte: Eine 3-Seiten-CRUD-App braucht keine vier Projekte und MediatR. Clean Architecture zahlt sich aus, wenn die Komplexität hoch und Wandel konstant ist.
  • Shared-Kernel-Missbrauch: Ein „Common"-Projekt, das jede Schicht referenziert und langsam zur Abladestelle für alles wird.

Wann sich Clean Architecture lohnt

Clean Architecture bringt strukturellen Mehraufwand mit sich. Sie lohnt sich, wenn:

  • Die Anwendung 3+ Jahre gepflegt wird
  • Mehrere Teams am Codebase arbeiten
  • Geschäftsregeln komplex und im Wandel sind
  • Sie Infrastruktur austauschen müssen (z. B. Migration von On-Premises in die Cloud)
  • Testbarkeit eine Geschäftsanforderung ist, nicht nur ein Nice-to-have

Für kurzlebige Anwendungen, Prototypen oder einfache CRUD-Systeme ist eine schlichtere Schichtarchitektur pragmatischer.

Bei CC Conceptualise unterstützen wir Enterprise-Teams bei der Einführung von Clean Architecture, passend zu ihrer Grösse, Komplexität und Teamreife. Wenn Sie eine neue .NET-Anwendung strukturieren oder eine bestehende refaktorieren, melden Sie sich bei uns.

Clean Architecture .NETEnterprise-AnwendungsstrukturDependency-Inversion-PrinzipCQRS MediatRDomain-Driven Design .NET

Brauchen Sie Expertenberatung?

Unser Team ist spezialisiert auf Cloud-Architektur, Security, KI-Plattformen und DevSecOps. Lassen Sie uns besprechen, wie wir Ihrem Unternehmen helfen können.

Verwandte Artikel