Clean Code Architecture C#: Vollständiger Leitfaden und Interview-Fragen 2026

Umfassender Leitfaden zur Clean Code Architecture in C# mit SOLID-Prinzipien, Schichtenarchitektur und den häufigsten technischen Interview-Fragen für .NET-Entwickler.

Clean Code Architecture C# Diagramm mit Schichten

Clean Code Architecture in C# verbindet Robert C. Martins Clean Code-Prinzipien mit dem Clean Architecture-Pattern, um wartbare, testbare und skalierbare .NET-Anwendungen zu entwickeln. Technische Interviews konzentrieren sich zunehmend auf diese Konzepte, da sie zeigen, wie Kandidaten über Software-Design nachdenken – jenseits der reinen Funktionalität.

Interview-Einblick

Bei Fragen zur Clean Architecture erwarten Interviewer, dass Kandidaten die Dependency Rule erklären können: Quellcode-Abhängigkeiten zeigen nach innen, zu höherwertigen Policies. Die Domain-Schicht weiß nichts über die Infrastruktur – nicht umgekehrt.

SOLID-Prinzipien als Fundament von Clean C# Code

SOLID-Prinzipien bilden das Rückgrat von Clean Code in C#. Die offizielle Microsoft-Dokumentation zu .NET-Grundlagen empfiehlt diese Patterns für Enterprise-Anwendungen. Jedes Prinzip adressiert ein spezifisches Wartungsproblem.

Single Responsibility Principle (SRP): Eine Klasse hat nur einen Grund zur Änderung. Der folgende OrderService verarbeitet ausschließlich Bestellungen und delegiert Persistenz sowie Benachrichtigungen an separate Komponenten.

OrderService.cscsharp
public class OrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly INotificationService _notificationService;

    public OrderService(
        IOrderRepository orderRepository,
        INotificationService notificationService)
    {
        _orderRepository = orderRepository;
        _notificationService = notificationService;
    }

    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        // Validiert und erstellt die Domain-Entität
        var order = Order.Create(request.CustomerId, request.Items);
        
        // Persistiert über die Repository-Abstraktion
        await _orderRepository.AddAsync(order);
        
        // Benachrichtigt über einen separaten Service
        await _notificationService.SendOrderConfirmationAsync(order);
        
        return order;
    }
}

Open/Closed Principle (OCP): Klassen bleiben offen für Erweiterungen, aber geschlossen für Modifikationen. Neue Zahlungsmethoden erfordern neue Klassen, keine Änderungen an bestehenden.

IPaymentProcessor.cscsharp
public interface IPaymentProcessor
{
    string PaymentMethod { get; }
    Task<PaymentResult> ProcessAsync(Payment payment);
}

// StripePaymentProcessor.cs
public class StripePaymentProcessor : IPaymentProcessor
{
    public string PaymentMethod => "Stripe";
    
    public async Task<PaymentResult> ProcessAsync(Payment payment)
    {
        // Stripe-spezifische Implementierung
        var charge = await _stripeClient.CreateChargeAsync(payment.Amount);
        return new PaymentResult(charge.Id, charge.Status == "succeeded");
    }
}

// PaymentService.cs
public class PaymentService
{
    private readonly IEnumerable<IPaymentProcessor> _processors;

    public PaymentService(IEnumerable<IPaymentProcessor> processors)
    {
        _processors = processors;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(
        string method, Payment payment)
    {
        var processor = _processors
            .FirstOrDefault(p => p.PaymentMethod == method)
            ?? throw new NotSupportedException($"Payment method {method} not supported");
        
        return await processor.ProcessAsync(payment);
    }
}

Liskov Substitution Principle (LSP): Unterklassen müssen für ihre Basisklassen substituierbar sein, ohne die Programmkorrektheit zu beeinträchtigen.

csharp
// Korrekte LSP-Implementierung
public abstract class Shape
{
    public abstract double CalculateArea();
}

public class Rectangle : Shape
{
    public double Width { get; init; }
    public double Height { get; init; }
    
    public override double CalculateArea() => Width * Height;
}

public class Circle : Shape
{
    public double Radius { get; init; }
    
    public override double CalculateArea() => Math.PI * Radius * Radius;
}

Interface Segregation Principle (ISP): Clients sollten nicht gezwungen werden, von Interfaces abzuhängen, die sie nicht nutzen.

csharp
// Schlechtes Beispiel - fettes Interface
public interface IUserService
{
    Task<User> GetByIdAsync(int id);
    Task CreateAsync(User user);
    Task UpdateAsync(User user);
    Task DeleteAsync(int id);
    Task SendEmailAsync(int userId, string message);
    Task GenerateReportAsync(int userId);
}

// Gutes Beispiel - segregierte Interfaces
public interface IUserReader
{
    Task<User> GetByIdAsync(int id);
}

public interface IUserWriter
{
    Task CreateAsync(User user);
    Task UpdateAsync(User user);
    Task DeleteAsync(int id);
}

public interface IUserNotifier
{
    Task SendEmailAsync(int userId, string message);
}

Dependency Inversion Principle (DIP): High-Level-Module sollten nicht von Low-Level-Modulen abhängen. Beide sollten von Abstraktionen abhängen.

csharp
// Domain Layer - definiert die Abstraktion
public interface IEmailSender
{
    Task SendAsync(string to, string subject, string body);
}

// Infrastructure Layer - implementiert die Abstraktion
public class SmtpEmailSender : IEmailSender
{
    private readonly SmtpSettings _settings;
    
    public SmtpEmailSender(IOptions<SmtpSettings> settings)
    {
        _settings = settings.Value;
    }
    
    public async Task SendAsync(string to, string subject, string body)
    {
        using var client = new SmtpClient(_settings.Host, _settings.Port);
        await client.SendMailAsync(new MailMessage(_settings.From, to, subject, body));
    }
}

Clean Architecture Layer in C# Anwendungen

Clean Architecture organisiert Code in konzentrischen Schichten mit strikten Abhängigkeitsregeln. Die innersten Schichten enthalten Business-Logik, während äußere Schichten Infrastruktur-Concerns behandeln.

Domain Layer (Innerste Schicht)

Der Domain Layer enthält Enterprise-Business-Regeln und Entitäten. Keine Abhängigkeiten zu externen Frameworks oder Bibliotheken.

Domain/Entities/Order.cscsharp
public class Order
{
    public Guid Id { get; private set; }
    public Guid CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    public Money TotalAmount { get; private set; }
    private readonly List<OrderItem> _items = new();
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();

    private Order() { } // Für EF Core

    public static Order Create(Guid customerId, IEnumerable<OrderItemRequest> items)
    {
        var order = new Order
        {
            Id = Guid.NewGuid(),
            CustomerId = customerId,
            Status = OrderStatus.Pending
        };

        foreach (var item in items)
        {
            order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);
        }

        return order;
    }

    public void AddItem(Guid productId, int quantity, decimal unitPrice)
    {
        if (Status != OrderStatus.Pending)
            throw new DomainException("Cannot modify a confirmed order");

        var item = new OrderItem(Id, productId, quantity, unitPrice);
        _items.Add(item);
        RecalculateTotal();
    }

    public void Confirm()
    {
        if (_items.Count == 0)
            throw new DomainException("Cannot confirm an empty order");

        Status = OrderStatus.Confirmed;
    }

    private void RecalculateTotal()
    {
        TotalAmount = Money.FromDecimal(
            _items.Sum(i => i.Quantity * i.UnitPrice.Amount),
            _items.First().UnitPrice.Currency);
    }
}

// Domain/ValueObjects/Money.cs
public record Money
{
    public decimal Amount { get; init; }
    public string Currency { get; init; }

    public static Money FromDecimal(decimal amount, string currency) =>
        new() { Amount = amount, Currency = currency };

    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new DomainException("Cannot add different currencies");
        return FromDecimal(Amount + other.Amount, Currency);
    }
}

Application Layer

Der Application Layer enthält Use Cases und orchestriert den Datenfluss zwischen Domain und äußeren Schichten.

Application/UseCases/CreateOrderUseCase.cscsharp
public class CreateOrderUseCase
{
    private readonly IOrderRepository _orderRepository;
    private readonly IProductRepository _productRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IEventPublisher _eventPublisher;

    public CreateOrderUseCase(
        IOrderRepository orderRepository,
        IProductRepository productRepository,
        IUnitOfWork unitOfWork,
        IEventPublisher eventPublisher)
    {
        _orderRepository = orderRepository;
        _productRepository = productRepository;
        _unitOfWork = unitOfWork;
        _eventPublisher = eventPublisher;
    }

    public async Task<Result<OrderDto>> ExecuteAsync(CreateOrderCommand command)
    {
        // Validiert Produktverfügbarkeit
        foreach (var item in command.Items)
        {
            var product = await _productRepository.GetByIdAsync(item.ProductId);
            if (product == null)
                return Result<OrderDto>.Failure($"Product {item.ProductId} not found");
            
            if (product.StockQuantity < item.Quantity)
                return Result<OrderDto>.Failure($"Insufficient stock for {product.Name}");
        }

        // Erstellt die Domain-Entität
        var order = Order.Create(command.CustomerId, command.Items);
        
        // Persistiert
        await _orderRepository.AddAsync(order);
        await _unitOfWork.SaveChangesAsync();
        
        // Publiziert Domain-Event
        await _eventPublisher.PublishAsync(new OrderCreatedEvent(order.Id));
        
        return Result<OrderDto>.Success(OrderDto.FromEntity(order));
    }
}

// Application/Interfaces/IOrderRepository.cs
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id);
    Task<IEnumerable<Order>> GetByCustomerIdAsync(Guid customerId);
    Task AddAsync(Order order);
    Task UpdateAsync(Order order);
}

Infrastructure Layer

Der Infrastructure Layer implementiert Interfaces, die in inneren Schichten definiert wurden.

Infrastructure/Persistence/OrderRepository.cscsharp
public class OrderRepository : IOrderRepository
{
    private readonly ApplicationDbContext _context;

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

    public async Task<Order?> GetByIdAsync(Guid id)
    {
        return await _context.Orders
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == id);
    }

    public async Task<IEnumerable<Order>> GetByCustomerIdAsync(Guid customerId)
    {
        return await _context.Orders
            .Include(o => o.Items)
            .Where(o => o.CustomerId == customerId)
            .ToListAsync();
    }

    public async Task AddAsync(Order order)
    {
        await _context.Orders.AddAsync(order);
    }

    public Task UpdateAsync(Order order)
    {
        _context.Orders.Update(order);
        return Task.CompletedTask;
    }
}

// Infrastructure/Persistence/ApplicationDbContext.cs
public class ApplicationDbContext : DbContext
{
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<Product> Products => Set<Product>();

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(
            typeof(ApplicationDbContext).Assembly);
    }
}

Dependency Injection für Clean Architecture

Dependency Injection ermöglicht die lose Kopplung zwischen Schichten. ASP.NET Core bietet hierfür einen integrierten DI-Container.

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

// Domain Services
builder.Services.AddScoped<IOrderDomainService, OrderDomainService>();

// Application Use Cases
builder.Services.AddScoped<CreateOrderUseCase>();
builder.Services.AddScoped<GetOrderByIdUseCase>();
builder.Services.AddScoped<CancelOrderUseCase>();

// Infrastructure
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();

// External Services
builder.Services.AddHttpClient<IPaymentGateway, StripePaymentGateway>();

var app = builder.Build();

Bereit für deine .NET-Interviews?

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

Häufige Interview-Fragen zur Clean Architecture

Technische Interviews testen das Verständnis von Clean Architecture durch konzeptuelle und praktische Fragen.

Frage: Erklären Sie die Dependency Rule in Clean Architecture.

Die Dependency Rule besagt, dass Quellcode-Abhängigkeiten nur nach innen zeigen dürfen. Äußere Schichten können innere Schichten referenzieren, aber niemals umgekehrt. Die Domain-Schicht bleibt völlig unabhängig von Frameworks, Datenbanken oder UI-Concerns.

Frage: Wie unterscheidet sich Clean Architecture von N-Tier-Architektur?

N-Tier-Architektur organisiert Code in horizontale Schichten (Presentation, Business, Data), wobei jede Schicht von der darunterliegenden abhängt. Clean Architecture verwendet konzentrische Schichten, bei denen Abhängigkeiten nach innen zeigen. Der Schlüsselunterschied besteht darin, dass in Clean Architecture die Business-Logik keine Kenntnis von der Datenzugriffsschicht hat.

Frage: Wann würden Sie Clean Architecture nicht verwenden?

Clean Architecture fügt Komplexität hinzu, die für einfache CRUD-Anwendungen oder Prototypen nicht gerechtfertigt sein kann. Kleine Projekte mit begrenztem Scope profitieren möglicherweise mehr von einfacheren Architekturen. Der Overhead lohnt sich bei langlebigen Enterprise-Anwendungen mit sich entwickelnden Anforderungen.

Teststrategien in Clean Architecture

Clean Architecture ermöglicht umfassende Tests durch klare Grenzen und Dependency Injection.

csharp
// Unit Test für Use Case
public class CreateOrderUseCaseTests
{
    private readonly Mock<IOrderRepository> _orderRepositoryMock;
    private readonly Mock<IProductRepository> _productRepositoryMock;
    private readonly Mock<IUnitOfWork> _unitOfWorkMock;
    private readonly Mock<IEventPublisher> _eventPublisherMock;
    private readonly CreateOrderUseCase _useCase;

    public CreateOrderUseCaseTests()
    {
        _orderRepositoryMock = new Mock<IOrderRepository>();
        _productRepositoryMock = new Mock<IProductRepository>();
        _unitOfWorkMock = new Mock<IUnitOfWork>();
        _eventPublisherMock = new Mock<IEventPublisher>();
        
        _useCase = new CreateOrderUseCase(
            _orderRepositoryMock.Object,
            _productRepositoryMock.Object,
            _unitOfWorkMock.Object,
            _eventPublisherMock.Object);
    }

    [Fact]
    public async Task ExecuteAsync_WithValidCommand_CreatesOrder()
    {
        // Arrange
        var productId = Guid.NewGuid();
        var product = new Product { Id = productId, StockQuantity = 100 };
        _productRepositoryMock
            .Setup(r => r.GetByIdAsync(productId))
            .ReturnsAsync(product);

        var command = new CreateOrderCommand
        {
            CustomerId = Guid.NewGuid(),
            Items = new[] { new OrderItemRequest(productId, 2, 29.99m) }
        };

        // Act
        var result = await _useCase.ExecuteAsync(command);

        // Assert
        Assert.True(result.IsSuccess);
        _orderRepositoryMock.Verify(r => r.AddAsync(It.IsAny<Order>()), Times.Once);
        _unitOfWorkMock.Verify(u => u.SaveChangesAsync(), Times.Once);
    }

    [Fact]
    public async Task ExecuteAsync_WithInsufficientStock_ReturnsFailure()
    {
        // Arrange
        var productId = Guid.NewGuid();
        var product = new Product { Id = productId, Name = "Widget", StockQuantity = 1 };
        _productRepositoryMock
            .Setup(r => r.GetByIdAsync(productId))
            .ReturnsAsync(product);

        var command = new CreateOrderCommand
        {
            CustomerId = Guid.NewGuid(),
            Items = new[] { new OrderItemRequest(productId, 10, 29.99m) }
        };

        // Act
        var result = await _useCase.ExecuteAsync(command);

        // Assert
        Assert.False(result.IsSuccess);
        Assert.Contains("Insufficient stock", result.Error);
    }
}

Fehlerbehandlung in Clean Architecture

Ein konsistenter Ansatz zur Fehlerbehandlung über alle Schichten hinweg verbessert die Wartbarkeit.

Domain/Exceptions/DomainException.cscsharp
public class DomainException : Exception
{
    public string Code { get; }
    
    public DomainException(string message, string code = "DOMAIN_ERROR")
        : base(message)
    {
        Code = code;
    }
}

// Application/Common/Result.cs
public class Result<T>
{
    public T? Value { get; }
    public string? Error { get; }
    public bool IsSuccess => Error == null;

    private Result(T value) => Value = value;
    private Result(string error) => Error = error;

    public static Result<T> Success(T value) => new(value);
    public static Result<T> Failure(string error) => new(error);

    public TResult Match<TResult>(
        Func<T, TResult> onSuccess,
        Func<string, TResult> onFailure) =>
        IsSuccess ? onSuccess(Value!) : onFailure(Error!);
}

// API/Controllers/OrdersController.cs
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly CreateOrderUseCase _createOrderUseCase;

    public OrdersController(CreateOrderUseCase createOrderUseCase)
    {
        _createOrderUseCase = createOrderUseCase;
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateOrderRequest request)
    {
        var command = new CreateOrderCommand
        {
            CustomerId = request.CustomerId,
            Items = request.Items.Select(i => new OrderItemRequest(
                i.ProductId, i.Quantity, i.UnitPrice)).ToArray()
        };

        var result = await _createOrderUseCase.ExecuteAsync(command);

        return result.Match<IActionResult>(
            success => CreatedAtAction(
                nameof(GetById), 
                new { id = success.Id }, 
                success),
            failure => BadRequest(new { error = failure }));
    }
}

Fazit

Clean Code Architecture in C# bietet einen strukturierten Ansatz für den Aufbau wartbarer .NET-Anwendungen. Durch die Befolgung der SOLID-Prinzipien und die klare Schichtentrennung können Teams Code entwickeln, der leicht zu testen, zu erweitern und langfristig zu warten ist. Das Verständnis dieser Konzepte bereitet nicht nur auf technische Interviews vor, sondern auch auf die praktischen Herausforderungen der Enterprise-Softwareentwicklung.

Die Investition in Clean Architecture zahlt sich bei größeren Projekten aus, bei denen sich Anforderungen ändern und Teams wachsen. Die anfängliche Komplexität wird durch die langfristigen Vorteile in Bezug auf Testbarkeit, Flexibilität und Codequalität mehr als ausgeglichen.

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 24. August 2026

Tags

#csharp
#clean-architecture
#solid-principles
#dotnet
#interview

Teilen

Verwandte Artikel