Clean Architecture in .NET 2026: CQRS, MediatR und Interview-Fragen für Entwickler

Ein umfassender Leitfaden zur Clean Architecture in .NET mit CQRS und MediatR. Inklusive Best Practices, Codebeispielen und häufigen Interview-Fragen für .NET-Architekten.

Clean Architecture in .NET mit CQRS und MediatR

Clean Architecture hat sich als bevorzugtes Architekturmuster für .NET-Anwendungen etabliert. In Kombination mit CQRS (Command Query Responsibility Segregation) und MediatR entstehen wartbare, testbare und skalierbare Systeme. Dieser Artikel beleuchtet die praktische Umsetzung dieser Patterns in .NET 9 und bereitet auf technische Interview-Fragen vor.

Clean Architecture trennt die Geschäftslogik von technischen Details wie Datenbanken und Frameworks. Diese Trennung ermöglicht unabhängige Tests und erleichtert zukünftige Technologiewechsel erheblich.

Was ist Clean Architecture?

Clean Architecture, auch bekannt als Onion Architecture oder Hexagonal Architecture, organisiert Code in konzentrischen Schichten. Die innerste Schicht enthält die Domänenlogik, während äußere Schichten für Infrastruktur und Präsentation zuständig sind.

Die Kernprinzipien umfassen:

  • Unabhängigkeit von Frameworks: Die Geschäftslogik kennt keine Frameworks
  • Testbarkeit: Geschäftsregeln lassen sich ohne UI, Datenbank oder externe Services testen
  • Unabhängigkeit von der UI: Die Benutzeroberfläche kann ohne Änderung der Geschäftsregeln ausgetauscht werden
  • Unabhängigkeit von der Datenbank: Oracle, SQL Server oder MongoDB sind austauschbar

Projektstruktur in .NET

Eine typische Clean Architecture Lösung in .NET besteht aus vier Hauptprojekten:

csharp
// Projektstruktur
MyApp.Domain/           // Entitäten, Value Objects, Domain Events
MyApp.Application/      // Use Cases, Commands, Queries, Interfaces
MyApp.Infrastructure/   // Datenbankzugriff, externe Services
MyApp.WebApi/          // Controller, Middleware, DI-Konfiguration

Die Abhängigkeitsrichtung zeigt immer nach innen. Infrastructure und WebApi referenzieren Application, aber niemals umgekehrt.

CQRS: Commands und Queries trennen

CQRS trennt Lese- und Schreiboperationen in separate Modelle. Commands verändern den Zustand, Queries liefern Daten ohne Seiteneffekte.

csharp
// Command Definition
public record CreateProductCommand(
    string Name,
    decimal Price,
    string Description
) : IRequest<ProductDto>;

// Query Definition
public record GetProductByIdQuery(Guid Id) : IRequest<ProductDto?>;

// Command Handler
public class CreateProductCommandHandler 
    : IRequestHandler<CreateProductCommand, ProductDto>
{
    private readonly IProductRepository _repository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateProductCommandHandler(
        IProductRepository repository,
        IUnitOfWork unitOfWork)
    {
        _repository = repository;
        _unitOfWork = unitOfWork;
    }

    public async Task<ProductDto> Handle(
        CreateProductCommand request,
        CancellationToken cancellationToken)
    {
        var product = new Product(
            request.Name,
            request.Price,
            request.Description);

        await _repository.AddAsync(product, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return product.ToDto();
    }
}

MediatR konfigurieren

MediatR implementiert das Mediator-Pattern und entkoppelt Sender von Empfängern. Die Konfiguration in .NET 9 erfolgt im Program.cs:

Program.cscsharp
var builder = WebApplication.CreateBuilder(args);

// MediatR registrieren
builder.Services.AddMediatR(cfg => {
    cfg.RegisterServicesFromAssembly(
        typeof(CreateProductCommand).Assembly);
    
    // Pipeline Behaviors hinzufügen
    cfg.AddBehavior<ValidationBehavior<,>>();
    cfg.AddBehavior<LoggingBehavior<,>>();
});

// FluentValidation für Commands
builder.Services.AddValidatorsFromAssembly(
    typeof(CreateProductCommandValidator).Assembly);

Pipeline Behaviors für Cross-Cutting Concerns

Pipeline Behaviors ermöglichen die Implementierung von Querschnittsbelangen wie Validierung, Logging und Caching:

csharp
public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(
        IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);
        
        var validationResults = await Task.WhenAll(
            _validators.Select(v => 
                v.ValidateAsync(context, cancellationToken)));

        var failures = validationResults
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Count != 0)
            throw new ValidationException(failures);

        return await next();
    }
}

Domain Events mit MediatR

Domain Events signalisieren wichtige Geschäftsereignisse und ermöglichen lose gekoppelte Reaktionen:

csharp
// Domain Event Definition
public record ProductCreatedEvent(Guid ProductId, string Name) 
    : INotification;

// Event Handler
public class ProductCreatedEventHandler 
    : INotificationHandler<ProductCreatedEvent>
{
    private readonly IEmailService _emailService;
    private readonly ILogger<ProductCreatedEventHandler> _logger;

    public ProductCreatedEventHandler(
        IEmailService emailService,
        ILogger<ProductCreatedEventHandler> logger)
    {
        _emailService = emailService;
        _logger = logger;
    }

    public async Task Handle(
        ProductCreatedEvent notification,
        CancellationToken cancellationToken)
    {
        _logger.LogInformation(
            "Produkt erstellt: {ProductName}", 
            notification.Name);

        await _emailService.SendProductNotificationAsync(
            notification.ProductId,
            cancellationToken);
    }
}

Repository Pattern und Unit of Work

Das Repository Pattern abstrahiert den Datenzugriff und macht die Domäne unabhängig von der Persistenzschicht:

csharp
// Interface im Application Layer
public interface IProductRepository
{
    Task<Product?> GetByIdAsync(
        Guid id, 
        CancellationToken cancellationToken = default);
    
    Task<IReadOnlyList<Product>> GetAllAsync(
        CancellationToken cancellationToken = default);
    
    Task AddAsync(
        Product product, 
        CancellationToken cancellationToken = default);
    
    void Update(Product product);
    void Delete(Product product);
}

// Implementation im Infrastructure Layer
public class ProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _context;

    public ProductRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<Product?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        return await _context.Products
            .Include(p => p.Category)
            .FirstOrDefaultAsync(
                p => p.Id == id, 
                cancellationToken);
    }

    public async Task AddAsync(
        Product product,
        CancellationToken cancellationToken = default)
    {
        await _context.Products.AddAsync(product, cancellationToken);
    }
}

Minimal APIs mit MediatR

.NET 9 ermöglicht elegante Minimal APIs in Kombination mit MediatR:

csharp
// Endpoint Definition
app.MapPost("/api/products", async (
    CreateProductCommand command,
    ISender sender,
    CancellationToken cancellationToken) =>
{
    var result = await sender.Send(command, cancellationToken);
    return Results.Created($"/api/products/{result.Id}", result);
})
.WithName("CreateProduct")
.WithOpenApi();

app.MapGet("/api/products/{id:guid}", async (
    Guid id,
    ISender sender,
    CancellationToken cancellationToken) =>
{
    var result = await sender.Send(
        new GetProductByIdQuery(id), 
        cancellationToken);
    
    return result is not null 
        ? Results.Ok(result) 
        : Results.NotFound();
})
.WithName("GetProductById")
.WithOpenApi();

Bereit für deine .NET-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Unit Testing mit Clean Architecture

Die Schichtentrennung vereinfacht das Testen erheblich:

csharp
public class CreateProductCommandHandlerTests
{
    private readonly Mock<IProductRepository> _repositoryMock;
    private readonly Mock<IUnitOfWork> _unitOfWorkMock;
    private readonly CreateProductCommandHandler _handler;

    public CreateProductCommandHandlerTests()
    {
        _repositoryMock = new Mock<IProductRepository>();
        _unitOfWorkMock = new Mock<IUnitOfWork>();
        _handler = new CreateProductCommandHandler(
            _repositoryMock.Object,
            _unitOfWorkMock.Object);
    }

    [Fact]
    public async Task Handle_ValidCommand_CreatesProduct()
    {
        // Arrange
        var command = new CreateProductCommand(
            "Test Produkt",
            99.99m,
            "Beschreibung");

        // Act
        var result = await _handler.Handle(
            command, 
            CancellationToken.None);

        // Assert
        Assert.NotNull(result);
        Assert.Equal(command.Name, result.Name);
        _repositoryMock.Verify(
            r => r.AddAsync(
                It.IsAny<Product>(), 
                It.IsAny<CancellationToken>()), 
            Times.Once);
        _unitOfWorkMock.Verify(
            u => u.SaveChangesAsync(It.IsAny<CancellationToken>()), 
            Times.Once);
    }
}

Häufige Interview-Fragen zur Clean Architecture

Frage 1: Was ist der Unterschied zwischen Clean Architecture und traditioneller N-Tier-Architektur?

In N-Tier-Architekturen zeigen Abhängigkeiten von oben nach unten: Presentation → Business → Data Access. Clean Architecture invertiert diese Abhängigkeit: Die Domäne steht im Zentrum und hat keine Abhängigkeiten zu äußeren Schichten. Infrastruktur implementiert Interfaces, die im Application Layer definiert sind.

Frage 2: Wann sollte CQRS eingesetzt werden?

CQRS eignet sich für Systeme mit unterschiedlichen Lese- und Schreibanforderungen. Typische Anwendungsfälle sind: hohe Leseanforderungen bei seltenen Schreibvorgängen, komplexe Domänenlogik bei Schreiboperationen, oder wenn verschiedene Lesemodelle für unterschiedliche Konsumenten benötigt werden.

Frage 3: Wie verhindert man zirkuläre Abhängigkeiten zwischen Layern?

Durch konsequente Anwendung des Dependency Inversion Principle: Interfaces werden im inneren Layer (Application) definiert, Implementierungen im äußeren Layer (Infrastructure). Die Dependency Injection konfiguriert die Zuordnung zur Laufzeit.

Frage 4: Was sind die Nachteile von Clean Architecture?

Höherer initialer Aufwand, mehr Boilerplate-Code, und erhöhte Komplexität für kleine Projekte. Die Vorteile überwiegen erst bei mittleren bis großen Anwendungen mit langfristiger Wartungsperspektive.

Frage 5: Wie werden Domain Events in verteilten Systemen behandelt?

Für verteilte Systeme reicht MediatR nicht aus. Integration Events werden über Message Broker wie RabbitMQ, Azure Service Bus oder Kafka verteilt. Das Outbox Pattern gewährleistet Konsistenz zwischen Datenbankoperationen und Event-Publishing.

Best Practices für 2026

  1. Vertical Slices in Kombination mit Clean Architecture: Features werden als eigenständige Einheiten organisiert, während die Schichtentrennung innerhalb jedes Slices beibehalten wird.

  2. Result Pattern statt Exceptions: Commands und Queries geben Result-Objekte zurück, die Erfolg oder Fehler explizit modellieren.

  3. Source Generators für Boilerplate: .NET Source Generators reduzieren repetitiven Code für Mappings und Validierungen.

  4. Aspire für Cloud-Native Entwicklung: .NET Aspire vereinfacht die Orchestrierung von Clean Architecture Anwendungen in containerisierten Umgebungen.

Fazit

Clean Architecture mit CQRS und MediatR bildet ein solides Fundament für skalierbare .NET-Anwendungen. Die klare Trennung von Verantwortlichkeiten erleichtert Wartung, Testing und Weiterentwicklung. Für technische Interviews ist das Verständnis der zugrundeliegenden Prinzipien ebenso wichtig wie die praktische Umsetzung. Die Investition in eine saubere Architektur zahlt sich langfristig durch reduzierte Komplexität und erhöhte Entwicklerproduktivität aus.

Tägliche Challenge

Findest du den Bug in .NET?

Ein echter Codeausschnitt, ein versteckter Bug, ein Versuch pro Tag. Zum Ausprobieren ohne Konto.

Anthony Fillion-Maillet

Geschrieben von

Anthony Fillion-Maillet

Gründer von SharpSkill

Seit über 10 Jahren Fullstack-Entwickler. Er leitet SharpSkill und verantwortet alles, was hier erscheint.

Aktualisiert am 15. September 2026

Tags

#clean-architecture
#cqrs
#mediatr
#dotnet
#architecture

Teilen

Verwandte Artikel