Clean Code Architecture C#: Complete Gids en Sollicitatievragen 2026
Uitgebreide gids over Clean Code Architecture in C# met SOLID-principes, gelaagde architectuur en de meest voorkomende technische sollicitatievragen voor .NET-ontwikkelaars.

Clean Code Architecture in C# combineert Robert C. Martins Clean Code-principes met het Clean Architecture-patroon om onderhoudbare, testbare en schaalbare .NET-applicaties te ontwikkelen. Technische sollicitatiegesprekken richten zich steeds meer op deze concepten omdat ze onthullen hoe een kandidaat nadenkt over softwareontwerp, verder dan alleen werkende code produceren.
Bij vragen over Clean Architecture verwachten interviewers dat kandidaten de Dependency Rule kunnen uitleggen: broncode-afhankelijkheden wijzen naar binnen, richting policies op hoger niveau. De Domain-laag weet niets over Infrastructure, niet andersom.
SOLID-principes als Basis van Clean C# Code
SOLID-principes vormen de ruggengraat van Clean Code in C#. De officiële Microsoft-documentatie over .NET-fundamentals beveelt deze patronen aan voor enterprise-applicaties. Elk principe adresseert een specifiek onderhoudsprobleem.
Single Responsibility Principle (SRP): Een klasse heeft slechts één reden om te veranderen. De onderstaande OrderService verwerkt uitsluitend bestellingen en delegeert persistentie en notificaties aan aparte componenten.
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)
{
// Valideert en creëert de domain-entiteit
var order = Order.Create(request.CustomerId, request.Items);
// Persisteert via de repository-abstractie
await _orderRepository.AddAsync(order);
// Notificeert via een aparte service
await _notificationService.SendOrderConfirmationAsync(order);
return order;
}
}Open/Closed Principle (OCP): Klassen blijven open voor uitbreiding maar gesloten voor modificatie. Nieuwe betaalmethoden vereisen nieuwe klassen, geen wijzigingen aan bestaande.
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-specifieke implementatie
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): Subklassen moeten substitueerbaar zijn voor hun basisklassen zonder de correctheid van het programma te beïnvloeden.
// Correcte LSP-implementatie
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 mogen niet gedwongen worden om afhankelijk te zijn van interfaces die ze niet gebruiken.
// Slecht voorbeeld - dikke 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);
}
// Goed voorbeeld - gesegregeerde 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 modules mogen niet afhankelijk zijn van low-level modules. Beide moeten afhankelijk zijn van abstracties.
// Domain Layer - definieert de abstractie
public interface IEmailSender
{
Task SendAsync(string to, string subject, string body);
}
// Infrastructure Layer - implementeert de abstractie
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 Lagen in C# Applicaties
Clean Architecture organiseert code in concentrische lagen met strikte afhankelijkheidsregels. De binnenste lagen bevatten businesslogica, terwijl buitenste lagen infrastructuurzaken afhandelen.
Domain Layer (Binnenste Laag)
De Domain Layer bevat enterprise-businessregels en entiteiten. Geen afhankelijkheden van externe frameworks of 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() { } // Voor 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
De Application Layer bevat use cases en orkestreert de datastroom tussen Domain en buitenste lagen.
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)
{
// Valideert productbeschikbaarheid
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}");
}
// Creëert de domain-entiteit
var order = Order.Create(command.CustomerId, command.Items);
// Persisteert
await _orderRepository.AddAsync(order);
await _unitOfWork.SaveChangesAsync();
// Publiceert 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
De Infrastructure Layer implementeert interfaces die in binnenste lagen zijn gedefinieerd.
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 voor Clean Architecture
Dependency Injection maakt losse koppeling tussen lagen mogelijk. ASP.NET Core biedt een ingebouwde 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();Klaar om je .NET gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Veelgestelde Sollicitatievragen over Clean Architecture
Technische sollicitatiegesprekken testen het begrip van Clean Architecture door conceptuele en praktische vragen.
Vraag: Leg de Dependency Rule in Clean Architecture uit.
De Dependency Rule stelt dat broncode-afhankelijkheden alleen naar binnen mogen wijzen. Buitenste lagen kunnen binnenste lagen refereren, maar nooit andersom. De Domain-laag blijft volledig onafhankelijk van frameworks, databases of UI-zorgen.
Vraag: Hoe verschilt Clean Architecture van N-Tier-architectuur?
N-Tier-architectuur organiseert code in horizontale lagen (Presentation, Business, Data), waarbij elke laag afhankelijk is van de onderliggende. Clean Architecture gebruikt concentrische lagen waarbij afhankelijkheden naar binnen wijzen. Het belangrijkste verschil is dat in Clean Architecture de businesslogica geen kennis heeft van de datalaag.
Vraag: Wanneer zou Clean Architecture niet gebruikt moeten worden?
Clean Architecture voegt complexiteit toe die mogelijk niet gerechtvaardigd is voor eenvoudige CRUD-applicaties of prototypes. Kleine projecten met beperkte scope kunnen meer baat hebben bij eenvoudigere architecturen. De overhead is gerechtvaardigd bij langlevende enterprise-applicaties met evoluerende requirements.
Teststrategieën in Clean Architecture
Clean Architecture maakt uitgebreide tests mogelijk door duidelijke grenzen en Dependency Injection.
// Unit Test voor 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);
}
}Foutafhandeling in Clean Architecture
Een consistente benadering van foutafhandeling over alle lagen verbetert de onderhoudbaarheid.
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 }));
}
}Conclusie
Clean Code Architecture in C# biedt een gestructureerde aanpak voor het bouwen van onderhoudbare .NET-applicaties. Door de SOLID-principes te volgen en een duidelijke laagscheiding te handhaven, kunnen teams code ontwikkelen die eenvoudig te testen, uit te breiden en op lange termijn te onderhouden is. Het begrijpen van deze concepten bereidt niet alleen voor op technische sollicitatiegesprekken, maar ook op de praktische uitdagingen van enterprise-softwareontwikkeling.
De investering in Clean Architecture betaalt zich uit bij grotere projecten waar requirements veranderen en teams groeien. De initiële complexiteit wordt ruimschoots gecompenseerd door de langetermijnvoordelen op het gebied van testbaarheid, flexibiliteit en codekwaliteit.
Zie jij de bug in .NET?
Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Geschreven door
Anthony Fillion-MailletOprichter van SharpSkill
Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.
Bijgewerkt op 24 augustus 2026
Tags
Delen
Gerelateerde artikelen

Geavanceerd C# LINQ in 2026: Operators, Performance en Sollicitatievragen
Een diepgaande gids over geavanceerde LINQ-operators, deferred execution, performance-optimalisatie en veelgestelde C# sollicitatievragen voor .NET-ontwikkelaars.

.NET 10 in 2026: Nieuwe Features, Native AOT en C# 14 voor Sollicitatiepreparatie
.NET 10 verschijnt als Long-Term Support release met Native AOT-verbeteringen, C# 14 extension members, de field keyword en file-based apps. Een complete gids over nieuwe features, prestatiewinst en interview-ready kennis voor .NET-ontwikkelaars in 2026.

Entity Framework Core: Prestatieoptimalisatie en Best Practices in 2026
Optimaliseer EF Core 10-prestaties met AsNoTracking, split queries, bulkbewerkingen, de nieuwe LeftJoin-operator en benoemde queryfilters. Praktische gids met C#-voorbeelden voor .NET 10-applicaties in productie.