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 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.
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.
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.
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.
// 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.
// 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.
// 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.
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.
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.
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.
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.
// 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.
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.
Findest du den Bug in .NET?
Ein echter Codeausschnitt, ein versteckter Bug, ein Versuch pro Tag. Zum Ausprobieren ohne Konto.

Geschrieben von
Anthony Fillion-MailletGründer von SharpSkill
Seit über 10 Jahren Fullstack-Entwickler. Er leitet SharpSkill und verantwortet alles, was hier erscheint.
Aktualisiert am 24. August 2026
Tags
Teilen
Verwandte Artikel

Fortgeschrittenes C# LINQ in 2026: Operatoren, Performance und Interviewfragen
Ein tiefgreifender Leitfaden zu fortgeschrittenen LINQ-Operatoren, deferred Execution, Performance-Optimierung und häufigen C#-Interviewfragen für .NET-Entwickler.

C# und .NET Interview-Fragen: Vollstaendiger Leitfaden 2026
Die 17 haeufigsten C#- und .NET-Interviewfragen. LINQ, async/await, Dependency Injection, Entity Framework Core und Best Practices mit ausfuehrlichen Antworten.

.NET 10 im Jahr 2026: Neue Features, Native AOT und Interviewfragen fuer Entwickler
.NET 10 als LTS-Version bringt produktionsreife Native-AOT-Kompilierung, C# 14 mit Extension Members und dem field-Keyword, dateibasierte Anwendungen und benannte Abfragefilter in EF Core 10.