Clean Architecture in .NET 2026: CQRS, MediatR en Interviewvragen voor Ontwikkelaars

Een uitgebreide gids over Clean Architecture in .NET met CQRS en MediatR. Inclusief best practices, codevoorbeelden en veelgestelde interviewvragen voor .NET-architecten.

Clean Architecture in .NET met CQRS en MediatR

Clean Architecture heeft zich gevestigd als het voorkeursarchitectuurpatroon voor .NET-applicaties. In combinatie met CQRS (Command Query Responsibility Segregation) en MediatR ontstaan onderhoudbare, testbare en schaalbare systemen. Dit artikel behandelt de praktische implementatie van deze patterns in .NET 9 en bereidt voor op technische sollicitatiegesprekken.

Clean Architecture scheidt businesslogica van technische details zoals databases en frameworks. Deze scheiding maakt onafhankelijke tests mogelijk en vergemakkelijkt toekomstige technologiewisselingen aanzienlijk.

Wat is Clean Architecture?

Clean Architecture, ook bekend als Onion Architecture of Hexagonal Architecture, organiseert code in concentrische lagen. De binnenste laag bevat de domeinlogica, terwijl buitenste lagen verantwoordelijk zijn voor infrastructuur en presentatie.

De kernprincipes omvatten:

  • Onafhankelijkheid van frameworks: De businesslogica kent geen frameworks
  • Testbaarheid: Businessregels kunnen worden getest zonder UI, database of externe services
  • Onafhankelijkheid van de UI: De gebruikersinterface kan worden vervangen zonder wijziging van businessregels
  • Onafhankelijkheid van de database: Oracle, SQL Server of MongoDB zijn uitwisselbaar

Projectstructuur in .NET

Een typische Clean Architecture-oplossing in .NET bestaat uit vier hoofdprojecten:

csharp
// Projectstructuur
MyApp.Domain/           // Entiteiten, Value Objects, Domain Events
MyApp.Application/      // Use Cases, Commands, Queries, Interfaces
MyApp.Infrastructure/   // Databasetoegang, externe services
MyApp.WebApi/          // Controllers, Middleware, DI-configuratie

De afhankelijkheidsrichting wijst altijd naar binnen. Infrastructure en WebApi refereren naar Application, maar nooit andersom.

CQRS: Commands en Queries Scheiden

CQRS scheidt lees- en schrijfoperaties in afzonderlijke modellen. Commands veranderen de staat, Queries leveren data zonder neveneffecten.

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

// Query Definitie
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 Configureren

MediatR implementeert het Mediator-pattern en ontkoppelt verzenders van ontvangers. De configuratie in .NET 9 gebeurt in Program.cs:

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

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

// FluentValidation voor Commands
builder.Services.AddValidatorsFromAssembly(
    typeof(CreateProductCommandValidator).Assembly);

Pipeline Behaviors voor Cross-Cutting Concerns

Pipeline Behaviors maken de implementatie van dwarsdoorsnijdende aspecten mogelijk zoals validatie, logging en 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 met MediatR

Domain Events signaleren belangrijke businessgebeurtenissen en maken losjes gekoppelde reacties mogelijk:

csharp
// Domain Event Definitie
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(
            "Product aangemaakt: {ProductName}", 
            notification.Name);

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

Repository Pattern en Unit of Work

Het Repository Pattern abstraheert de datatoegang en maakt het domein onafhankelijk van de persistentielaag:

csharp
// Interface in de 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);
}

// Implementatie in de 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 met MediatR

.NET 9 maakt elegante Minimal APIs mogelijk in combinatie met MediatR:

csharp
// Endpoint Definitie
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();

Klaar om je .NET gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Unit Testing met Clean Architecture

De scheiding in lagen vereenvoudigt het testen aanzienlijk:

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 Product",
            99.99m,
            "Beschrijving");

        // 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);
    }
}

Veelgestelde Interviewvragen over Clean Architecture

Vraag 1: Wat is het verschil tussen Clean Architecture en traditionele N-Tier-architectuur?

In N-Tier-architecturen wijzen afhankelijkheden van boven naar beneden: Presentation → Business → Data Access. Clean Architecture inverteert deze afhankelijkheid: het Domein staat centraal en heeft geen afhankelijkheden naar buitenste lagen. Infrastructure implementeert interfaces die in de Application Layer zijn gedefinieerd.

Vraag 2: Wanneer moet CQRS worden toegepast?

CQRS is geschikt voor systemen met verschillende lees- en schrijfvereisten. Typische toepassingen zijn: hoge leesbelasting bij zeldzame schrijfoperaties, complexe domeinlogica bij schrijfoperaties, of wanneer verschillende leesmodellen nodig zijn voor verschillende consumenten.

Vraag 3: Hoe worden circulaire afhankelijkheden tussen lagen voorkomen?

Door consequente toepassing van het Dependency Inversion Principle: interfaces worden gedefinieerd in de binnenste laag (Application), implementaties in de buitenste laag (Infrastructure). Dependency Injection configureert de toewijzing tijdens runtime.

Vraag 4: Wat zijn de nadelen van Clean Architecture?

Hogere initiële inspanning, meer boilerplate-code en verhoogde complexiteit voor kleine projecten. De voordelen wegen pas op bij middelgrote tot grote applicaties met langetermijnonderhoudsperspectief.

Vraag 5: Hoe worden Domain Events behandeld in gedistribueerde systemen?

Voor gedistribueerde systemen is MediatR niet voldoende. Integration Events worden verspreid via message brokers zoals RabbitMQ, Azure Service Bus of Kafka. Het Outbox Pattern waarborgt consistentie tussen databaseoperaties en event-publishing.

Best Practices voor 2026

  1. Vertical Slices in combinatie met Clean Architecture: Features worden georganiseerd als zelfstandige eenheden, terwijl de scheiding in lagen binnen elke slice behouden blijft.

  2. Result Pattern in plaats van Exceptions: Commands en Queries retourneren Result-objecten die succes of falen expliciet modelleren.

  3. Source Generators voor boilerplate: .NET Source Generators verminderen repetitieve code voor mappings en validaties.

  4. Aspire voor Cloud-Native ontwikkeling: .NET Aspire vereenvoudigt de orkestratie van Clean Architecture-applicaties in gecontaineriseerde omgevingen.

Conclusie

Clean Architecture met CQRS en MediatR vormt een solide fundament voor schaalbare .NET-applicaties. De duidelijke scheiding van verantwoordelijkheden vergemakkelijkt onderhoud, testen en doorontwikkeling. Voor technische sollicitatiegesprekken is het begrip van de onderliggende principes even belangrijk als de praktische implementatie. De investering in een schone architectuur betaalt zich op lange termijn terug door verminderde complexiteit en verhoogde ontwikkelaarsproductiviteit.

Dagelijkse challenge

Zie jij de bug in .NET?

Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Anthony Fillion-Maillet

Geschreven door

Anthony Fillion-Maillet

Oprichter van SharpSkill

Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.

Bijgewerkt op 15 september 2026

Tags

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

Delen

Gerelateerde artikelen