Clean Architecture mit .NET cover image

Clean Architecture mit .NET

Von Julian Krause · Dienstag, 1. Juli 2025 · ~3 Min. Lesezeit

Clean Architecture, popularisiert von Robert C. Martin, liefert eine Abhängigkeitsstruktur, die Ihre Geschäftslogik unabhängig von Frameworks, Datenbanken und externen Diensten hält. So setze ich sie in .NET-Projekten um, samt der Kompromisse, die Sie dabei bedenken müssen.

Die Schichtstruktur

In einem typischen .NET-Clean-Architecture-Projekt verwende ich vier Schichten mit strikten Abhängigkeitsregeln. Abhängigkeiten zeigen immer nach innen.

src/
  MyApp.Domain/          # Entities, value objects, domain events (no dependencies)
  MyApp.Application/     # Use cases, interfaces, DTOs (depends on Domain)
  MyApp.Infrastructure/  # EF Core, external APIs, file system (depends on Application)
  MyApp.Api/             # Controllers, middleware, DI composition (depends on all)

Die entscheidende Regel: Domain- und Application-Schicht haben keinerlei Bezug zu Infrastruktur-Belangen. Keine EF-Core-Attribute an Entities. Kein HttpClient in Use Cases. Keine IConfiguration in der Domänenlogik.

Domain-Schicht

Die Domain-Schicht enthält Ihre Entities und Geschäftsregeln ganz ohne externe Abhängigkeiten.

// Domain/Entities/Order.cs
public class Order
{
    public Guid Id { get; private set; }
    public CustomerId CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    private readonly List<OrderLine> _lines = new();
    public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();

    public Money Total => _lines.Aggregate(
        Money.Zero("EUR"),
        (sum, line) => sum + line.LineTotal);

    public void AddLine(ProductId productId, int quantity, Money unitPrice)
    {
        if (Status != OrderStatus.Draft)
            throw new DomainException("Cannot modify a submitted order.");

        _lines.Add(new OrderLine(productId, quantity, unitPrice));
    }

    public void Submit()
    {
        if (!_lines.Any())
            throw new DomainException("Cannot submit an empty order.");

        Status = OrderStatus.Submitted;
    }
}

Application-Schicht

Die Application-Schicht definiert die Use Cases und die Schnittstellen, die die Infrastruktur implementieren muss.

// Application/Interfaces/IOrderRepository.cs
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task AddAsync(Order order, CancellationToken ct = default);
    Task SaveChangesAsync(CancellationToken ct = default);
}

// Application/UseCases/SubmitOrderCommand.cs
public record SubmitOrderCommand(Guid OrderId) : IRequest<Result>;

public class SubmitOrderHandler : IRequestHandler<SubmitOrderCommand, Result>
{
    private readonly IOrderRepository _orders;
    private readonly IEventPublisher _events;

    public SubmitOrderHandler(IOrderRepository orders, IEventPublisher events)
    {
        _orders = orders;
        _events = events;
    }

    public async Task<Result> Handle(SubmitOrderCommand request, CancellationToken ct)
    {
        var order = await _orders.GetByIdAsync(request.OrderId, ct);
        if (order is null) return Result.NotFound();

        order.Submit();
        await _orders.SaveChangesAsync(ct);
        await _events.PublishAsync(new OrderSubmitted(order.Id), ct);

        return Result.Success();
    }
}

Infrastruktur-Schicht

Die Infrastruktur implementiert die von der Application-Schicht definierten Schnittstellen.

// Infrastructure/Persistence/OrderRepository.cs
public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

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

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

    public async Task AddAsync(Order order, CancellationToken ct)
        => await _context.Orders.AddAsync(order, ct);

    public async Task SaveChangesAsync(CancellationToken ct)
        => await _context.SaveChangesAsync(ct);
}

Der DI-Composition-Root

Das API-Projekt dient als Composition Root, in dem alles miteinander verdrahtet wird.

// Api/Program.cs
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IEventPublisher, MediatREventPublisher>();
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssembly(typeof(SubmitOrderHandler).Assembly));

Wann Clean Architecture übertrieben ist

Nicht jedes Projekt braucht diesen Grad an Trennung. Für eine einfache CRUD-API mit minimaler Geschäftslogik bringt Clean Architecture Zeremoniell ohne proportionalen Nutzen. Ich greife darauf zurück, wenn die Domäne komplex genug ist, dass ich Geschäftsregeln wirklich isoliert testen muss, oder wenn ich erwarte, dass sich die Infrastruktur im Laufe der Zeit ändert.

Das Ziel ist nicht architektonische Reinheit. Das Ziel ist wartbare Software, die sich mit sich ändernden Anforderungen weiterentwickeln kann.


Kommentare (3)

David Sonntag, 22. Februar 2026 17:08

Could you elaborate on this topic in a follow-up post?

Greta Donnerstag, 19. Februar 2026 20:08

Good question, I'd like to know too.

Hannah Samstag, 7. März 2026 23:08

Exactly! I had the same thought.

Elena Montag, 23. Februar 2026 17:08

This is exactly what I was looking for, thank you!

Felix Dienstag, 24. Februar 2026 17:08

I have a question: does this also apply to older versions?

Kommentar hinterlassen