# Clean Architecture met .NET: Praktische Gids > Beheers Clean Architecture in .NET met C#. Leer de SOLID-principes, laagscheiding en implementatiepatronen voor onderhoudbare applicaties. - Published: 2026-02-13 - Updated: 2026-04-28 - Author: SharpSkill - Tags: dotnet, clean architecture, csharp, aspnet core, design patterns - Reading time: 16 min --- Clean Architecture, populair gemaakt door Robert C. Martin (Uncle Bob), organiseert de code zo dat de bedrijfslogica in het hart van de applicatie staat, onafhankelijk van frameworks en implementatiedetails. Deze architectuuraanpak garandeert testbaarheid, onderhoudbaarheid en schaalbaarheid voor .NET-applicaties. Deze gids toont een praktische implementatie met ASP.NET Core. > **Waarom Clean Architecture?** > > Applicaties die bedrijfslogica vermengen met infrastructuurcode worden snel moeilijk te onderhouden. Clean Architecture dwingt een strikte scheiding af waardoor technische details gewijzigd kunnen worden zonder de bedrijfskern te raken. ## Kernprincipes van Clean Architecture Clean Architecture leunt op dependency inversion: interne lagen weten niets van externe lagen. Het bedrijfsdomein blijft geïsoleerd en kan onafhankelijk evolueren van technische keuzes zoals het webframework of de database. ``` ┌─────────────────────────────────────────────────────────┐ │ Presentation │ │ (Controllers, Razor Pages, Blazor, API Endpoints) │ ├─────────────────────────────────────────────────────────┤ │ Infrastructure │ │ (EF Core, External APIs, File System, Email) │ ├─────────────────────────────────────────────────────────┤ │ Application │ │ (Use Cases, Commands, Queries, DTOs) │ ├─────────────────────────────────────────────────────────┤ │ Domain │ │ (Entities, Value Objects, Domain Services) │ └─────────────────────────────────────────────────────────┘ ``` Afhankelijkheden wijzen altijd naar binnen: Presentation → Infrastructure → Application → Domain. De Domain refereert aan geen enkel ander project. ## .NET-projectstructuur voor Clean Architecture De projectorganisatie weerspiegelt de verschillende lagen. Elke laag komt overeen met een apart project in de Visual Studio-solution, wat zorgt voor een fysieke scheiding van verantwoordelijkheden. ```bash # terminal # Solution-structuur aanmaken dotnet new sln -n CleanArchitecture # Projecten per laag aanmaken dotnet new classlib -n CleanArchitecture.Domain -o src/CleanArchitecture.Domain dotnet new classlib -n CleanArchitecture.Application -o src/CleanArchitecture.Application dotnet new classlib -n CleanArchitecture.Infrastructure -o src/CleanArchitecture.Infrastructure dotnet new webapi -n CleanArchitecture.Api -o src/CleanArchitecture.Api # Projecten aan de solution toevoegen dotnet sln add src/CleanArchitecture.Domain dotnet sln add src/CleanArchitecture.Application dotnet sln add src/CleanArchitecture.Infrastructure dotnet sln add src/CleanArchitecture.Api # Projectreferenties configureren cd src/CleanArchitecture.Application dotnet add reference ../CleanArchitecture.Domain cd ../CleanArchitecture.Infrastructure dotnet add reference ../CleanArchitecture.Application cd ../CleanArchitecture.Api dotnet add reference ../CleanArchitecture.Infrastructure ``` Deze structuur garandeert dat de Domain onafhankelijk blijft en dat afhankelijkheden de Dependency Rule van Clean Architecture volgen. ## De Domain-laag: de bedrijfskern De Domain-laag bevat de bedrijfsentiteiten, Value Objects en repository-interfaces. In deze laag zijn geen externe afhankelijkheden toegestaan. ```csharp // Domain/Entities/Order.cs namespace CleanArchitecture.Domain.Entities; // Entiteit met identiteit en bedrijfslevenscyclus public class Order { // Unieke order-identifier public Guid Id { get; private set; } // Klantreferentie (Value Object voor het e-mailadres) public string CustomerEmail { get; private set; } // Item-collectie (one-to-many-relatie) private readonly List _items = new(); public IReadOnlyCollection Items => _items.AsReadOnly(); // Orderstatus (bedrijfs-enum) public OrderStatus Status { get; private set; } // Berekend totaalbedrag public decimal TotalAmount => _items.Sum(i => i.TotalPrice); // Tracking-data public DateTime CreatedAt { get; private set; } public DateTime? ShippedAt { get; private set; } // Privé-constructor dwingt het gebruik van de factory method af private Order() { } // Factory method om een geldige order aan te maken public static Order Create(string customerEmail) { // Validatie van bedrijfsregels bij aanmaak if (string.IsNullOrWhiteSpace(customerEmail)) throw new DomainException("Het e-mailadres van de klant is verplicht."); if (!IsValidEmail(customerEmail)) throw new DomainException("Ongeldig e-mailformaat."); return new Order { Id = Guid.NewGuid(), CustomerEmail = customerEmail, Status = OrderStatus.Pending, CreatedAt = DateTime.UtcNow }; } // Bedrijfsmethode: een item toevoegen public void AddItem(Product product, int quantity) { // Bedrijfsregel: een verzonden order kan niet worden gewijzigd if (Status == OrderStatus.Shipped) throw new DomainException("Een reeds verzonden order kan niet worden gewijzigd."); if (quantity <= 0) throw new DomainException("De hoeveelheid moet positief zijn."); // Controleren of het product al bestaat var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id); if (existingItem != null) { existingItem.IncreaseQuantity(quantity); } else { _items.Add(OrderItem.Create(this, product, quantity)); } } // Bedrijfsmethode: de order bevestigen public void Confirm() { if (Status != OrderStatus.Pending) throw new DomainException("Alleen openstaande orders kunnen worden bevestigd."); if (!_items.Any()) throw new DomainException("Een order moet ten minste één item bevatten."); Status = OrderStatus.Confirmed; } // Bedrijfsmethode: de order verzenden public void Ship() { if (Status != OrderStatus.Confirmed) throw new DomainException("De order moet worden bevestigd voordat hij wordt verzonden."); Status = OrderStatus.Shipped; ShippedAt = DateTime.UtcNow; } private static bool IsValidEmail(string email) => email.Contains('@') && email.Contains('.'); } ``` De Order-entiteit kapselt zijn bedrijfsregels in en beschermt zijn interne staat. Wijzigingen moeten verlopen via bedrijfsmethoden die de invarianten valideren. ```csharp // Domain/Entities/OrderItem.cs namespace CleanArchitecture.Domain.Entities; // Kindentiteit met eigen identiteit public class OrderItem { public Guid Id { get; private set; } public Guid OrderId { get; private set; } public Guid ProductId { get; private set; } public string ProductName { get; private set; } public decimal UnitPrice { get; private set; } public int Quantity { get; private set; } // Berekening van de regeltotaalprijs public decimal TotalPrice => UnitPrice * Quantity; private OrderItem() { } // Factory method met validatie public static OrderItem Create(Order order, Product product, int quantity) { return new OrderItem { Id = Guid.NewGuid(), OrderId = order.Id, ProductId = product.Id, ProductName = product.Name, UnitPrice = product.Price, Quantity = quantity }; } // Methode om de hoeveelheid te verhogen public void IncreaseQuantity(int additionalQuantity) { if (additionalQuantity <= 0) throw new DomainException("De extra hoeveelheid moet positief zijn."); Quantity += additionalQuantity; } } ``` ```csharp // Domain/Enums/OrderStatus.cs namespace CleanArchitecture.Domain.Enums; // Opsomming van orderstatussen public enum OrderStatus { Pending = 0, // Wacht op bevestiging Confirmed = 1, // Bevestigd, klaar voor verzending Shipped = 2, // Verzonden Delivered = 3, // Geleverd Cancelled = 4 // Geannuleerd } ``` > **Value Objects** > > Value Objects zoals Money, Address of Email kapselen bedrijfsbegrippen in zonder eigen identiteit. Hun gelijkheid is gebaseerd op waarden, niet op referenties. Deze aanpak versterkt de uitdrukkingskracht van het domein. ## Repository-interfaces in de Domain De repository-interfaces worden gedefinieerd in de Domain, maar hun implementaties bevinden zich in Infrastructure. Dit patroon respecteert het principe van dependency inversion. ```csharp // Domain/Interfaces/IOrderRepository.cs namespace CleanArchitecture.Domain.Interfaces; // Interface van de Order-repository public interface IOrderRepository { // Opvragen op identifier Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); // Opvragen inclusief items Task GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default); // Opvragen op klant-e-mail Task> GetByCustomerEmailAsync( string email, CancellationToken cancellationToken = default); // Een nieuwe order toevoegen Task AddAsync(Order order, CancellationToken cancellationToken = default); // Een bestaande order bijwerken Task UpdateAsync(Order order, CancellationToken cancellationToken = default); // Een order verwijderen Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); } ``` ```csharp // Domain/Interfaces/IUnitOfWork.cs namespace CleanArchitecture.Domain.Interfaces; // Unit-of-Work-patroon voor transactioneel beheer public interface IUnitOfWork : IDisposable { // Repositories bereikbaar via UoW IOrderRepository Orders { get; } IProductRepository Products { get; } // Atomair opslaan van alle wijzigingen Task SaveChangesAsync(CancellationToken cancellationToken = default); // Expliciet transactiebeheer Task BeginTransactionAsync(CancellationToken cancellationToken = default); Task CommitTransactionAsync(CancellationToken cancellationToken = default); Task RollbackTransactionAsync(CancellationToken cancellationToken = default); } ``` Het Unit-of-Work-patroon coördineert bewerkingen over meerdere repositories binnen één transactie. ## De Application-laag: orkestratie van Use Cases De Application-laag bevat de applicatielogica (use cases), DTO's en interfaces van externe diensten. Deze laag orkestreert de interacties tussen het domein en de buitenwereld. ```csharp // Application/Orders/Commands/CreateOrderCommand.cs using MediatR; namespace CleanArchitecture.Application.Orders.Commands; // Command dat de intentie uitdrukt om een order aan te maken public record CreateOrderCommand( string CustomerEmail, List Items ) : IRequest; // DTO voor de orderitems public record OrderItemDto( Guid ProductId, int Quantity ); ``` ```csharp // Application/Orders/Commands/CreateOrderCommandHandler.cs using MediatR; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Interfaces; namespace CleanArchitecture.Application.Orders.Commands; // Handler die de use case implementeert public class CreateOrderCommandHandler : IRequestHandler { private readonly IUnitOfWork _unitOfWork; public CreateOrderCommandHandler(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } public async Task Handle( CreateOrderCommand request, CancellationToken cancellationToken) { // Order-entiteit aanmaken via factory method var order = Order.Create(request.CustomerEmail); // Items aan de order toevoegen foreach (var item in request.Items) { // Product ophalen uit het repository var product = await _unitOfWork.Products .GetByIdAsync(item.ProductId, cancellationToken); if (product == null) throw new NotFoundException($"Product {item.ProductId} niet gevonden."); // Bedrijfsmethode van de entiteit gebruiken order.AddItem(product, item.Quantity); } // Persisteren via repository await _unitOfWork.Orders.AddAsync(order, cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken); return order.Id; } } ``` MediatR implementeert het Mediator-patroon om handlers los te koppelen van controllers. Elk command heeft één handler die verantwoordelijk is voor de verwerking. ```csharp // Application/Orders/Queries/GetOrderByIdQuery.cs using MediatR; namespace CleanArchitecture.Application.Orders.Queries; // Query om een order op ID op te vragen public record GetOrderByIdQuery(Guid OrderId) : IRequest; // Antwoord-DTO voor een order public record OrderDto( Guid Id, string CustomerEmail, string Status, decimal TotalAmount, DateTime CreatedAt, DateTime? ShippedAt, List Items ); // DTO voor items in het antwoord public record OrderItemResponseDto( Guid Id, string ProductName, decimal UnitPrice, int Quantity, decimal TotalPrice ); ``` ```csharp // Application/Orders/Queries/GetOrderByIdQueryHandler.cs using MediatR; using CleanArchitecture.Domain.Interfaces; namespace CleanArchitecture.Application.Orders.Queries; public class GetOrderByIdQueryHandler : IRequestHandler { private readonly IOrderRepository _orderRepository; public GetOrderByIdQueryHandler(IOrderRepository orderRepository) { _orderRepository = orderRepository; } public async Task Handle( GetOrderByIdQuery request, CancellationToken cancellationToken) { // Ophalen inclusief items var order = await _orderRepository .GetByIdWithItemsAsync(request.OrderId, cancellationToken); if (order == null) return null; // Mappen naar DTO (handmatige projectie) return new OrderDto( Id: order.Id, CustomerEmail: order.CustomerEmail, Status: order.Status.ToString(), TotalAmount: order.TotalAmount, CreatedAt: order.CreatedAt, ShippedAt: order.ShippedAt, Items: order.Items.Select(i => new OrderItemResponseDto( Id: i.Id, ProductName: i.ProductName, UnitPrice: i.UnitPrice, Quantity: i.Quantity, TotalPrice: i.TotalPrice )).ToList() ); } } ``` De scheiding tussen Commands en Queries volgt het CQRS-patroon (Command Query Responsibility Segregation) en optimaliseert lees- en schrijfacties onafhankelijk van elkaar. ## Validatie met FluentValidation De validatie van commands gebeurt in de Application-laag, vóór de uitvoering van de handler. ```csharp // Application/Orders/Validators/CreateOrderCommandValidator.cs using FluentValidation; using CleanArchitecture.Application.Orders.Commands; namespace CleanArchitecture.Application.Orders.Validators; public class CreateOrderCommandValidator : AbstractValidator { public CreateOrderCommandValidator() { // E-mail verplicht en geldig formaat RuleFor(x => x.CustomerEmail) .NotEmpty().WithMessage("Het e-mailadres van de klant is verplicht.") .EmailAddress().WithMessage("Ongeldig e-mailformaat."); // Minimaal één item verplicht RuleFor(x => x.Items) .NotEmpty().WithMessage("De order moet minstens één item bevatten."); // Elk item valideren RuleForEach(x => x.Items).ChildRules(item => { item.RuleFor(i => i.ProductId) .NotEmpty().WithMessage("De product-identifier is verplicht."); item.RuleFor(i => i.Quantity) .GreaterThan(0).WithMessage("De hoeveelheid moet positief zijn.") .LessThanOrEqualTo(100).WithMessage("Maximale hoeveelheid: 100."); }); } } ``` ```csharp // Application/Common/Behaviors/ValidationBehavior.cs using FluentValidation; using MediatR; namespace CleanArchitecture.Application.Common.Behaviors; // Pipeline behavior voor automatische validatie public class ValidationBehavior : IPipelineBehavior where TRequest : notnull { private readonly IEnumerable> _validators; public ValidationBehavior(IEnumerable> validators) { _validators = validators; } public async Task Handle( TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { // Als er geen validators zijn, doorgaan if (!_validators.Any()) return await next(); // Alle validators uitvoeren var context = new ValidationContext(request); var validationResults = await Task.WhenAll( _validators.Select(v => v.ValidateAsync(context, cancellationToken))); // Fouten samenvoegen var failures = validationResults .SelectMany(r => r.Errors) .Where(f => f != null) .ToList(); // Exception gooien als er fouten zijn if (failures.Any()) throw new ValidationException(failures); return await next(); } } ``` Het ValidationBehavior wordt automatisch uitgevoerd vóór elke handler en zorgt ervoor dat alleen geldige commands de bedrijfslogica bereiken. ## De Infrastructure-laag: technische implementatie De Infrastructure-laag levert de concrete implementaties van de in Domain en Application gedefinieerde interfaces. ```csharp // Infrastructure/Persistence/AppDbContext.cs using Microsoft.EntityFrameworkCore; using CleanArchitecture.Domain.Entities; namespace CleanArchitecture.Infrastructure.Persistence; public class AppDbContext : DbContext { public AppDbContext(DbContextOptions options) : base(options) { } public DbSet Orders => Set(); public DbSet OrderItems => Set(); public DbSet Products => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { // Configuraties uit het assembly toepassen modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); } } ``` ```csharp // Infrastructure/Persistence/Configurations/OrderConfiguration.cs using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using CleanArchitecture.Domain.Entities; namespace CleanArchitecture.Infrastructure.Persistence.Configurations; public class OrderConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { // Tabel en primary key builder.ToTable("Orders"); builder.HasKey(o => o.Id); // Eigenschappen builder.Property(o => o.CustomerEmail) .IsRequired() .HasMaxLength(256); builder.Property(o => o.Status) .IsRequired() .HasConversion(); // Opslag als leesbare string // Relatie met OrderItems builder.HasMany(o => o.Items) .WithOne() .HasForeignKey(i => i.OrderId) .OnDelete(DeleteBehavior.Cascade); // Index voor zoeken op e-mail builder.HasIndex(o => o.CustomerEmail); // Toegang tot het privéveld _items builder.Navigation(o => o.Items) .UsePropertyAccessMode(PropertyAccessMode.Field); } } ``` ```csharp // Infrastructure/Repositories/OrderRepository.cs using Microsoft.EntityFrameworkCore; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Infrastructure.Persistence; namespace CleanArchitecture.Infrastructure.Repositories; public class OrderRepository : IOrderRepository { private readonly AppDbContext _context; public OrderRepository(AppDbContext context) { _context = context; } public async Task GetByIdAsync( Guid id, CancellationToken cancellationToken = default) { return await _context.Orders .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); } public async Task GetByIdWithItemsAsync( Guid id, CancellationToken cancellationToken = default) { // Eager loading van de items return await _context.Orders .Include(o => o.Items) .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); } public async Task> GetByCustomerEmailAsync( string email, CancellationToken cancellationToken = default) { return await _context.Orders .Include(o => o.Items) .Where(o => o.CustomerEmail == email) .OrderByDescending(o => o.CreatedAt) .ToListAsync(cancellationToken); } public async Task AddAsync( Order order, CancellationToken cancellationToken = default) { await _context.Orders.AddAsync(order, cancellationToken); } public Task UpdateAsync( Order order, CancellationToken cancellationToken = default) { // EF Core houdt wijzigingen automatisch bij _context.Orders.Update(order); return Task.CompletedTask; } public async Task DeleteAsync( Guid id, CancellationToken cancellationToken = default) { await _context.Orders .Where(o => o.Id == id) .ExecuteDeleteAsync(cancellationToken); } } ``` > **Pas op voor leaky abstractions** > > Repositories mogen IQueryable niet rechtstreeks blootstellen, omdat dat een afhankelijkheid van EF Core in de bovenliggende lagen creëert. Specifieke methoden met duidelijke parameters verdienen de voorkeur. ## Implementatie van Unit of Work ```csharp // Infrastructure/Persistence/UnitOfWork.cs using Microsoft.EntityFrameworkCore.Storage; using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Infrastructure.Repositories; namespace CleanArchitecture.Infrastructure.Persistence; public class UnitOfWork : IUnitOfWork { private readonly AppDbContext _context; private IDbContextTransaction? _transaction; // Lazy loading van repositories private IOrderRepository? _orderRepository; private IProductRepository? _productRepository; public UnitOfWork(AppDbContext context) { _context = context; } // Aanmaak op aanvraag (lazy initialization) public IOrderRepository Orders => _orderRepository ??= new OrderRepository(_context); public IProductRepository Products => _productRepository ??= new ProductRepository(_context); public async Task SaveChangesAsync( CancellationToken cancellationToken = default) { return await _context.SaveChangesAsync(cancellationToken); } public async Task BeginTransactionAsync( CancellationToken cancellationToken = default) { _transaction = await _context.Database .BeginTransactionAsync(cancellationToken); } public async Task CommitTransactionAsync( CancellationToken cancellationToken = default) { if (_transaction == null) throw new InvalidOperationException("Geen actieve transactie."); await _transaction.CommitAsync(cancellationToken); await _transaction.DisposeAsync(); _transaction = null; } public async Task RollbackTransactionAsync( CancellationToken cancellationToken = default) { if (_transaction == null) throw new InvalidOperationException("Geen actieve transactie."); await _transaction.RollbackAsync(cancellationToken); await _transaction.DisposeAsync(); _transaction = null; } public void Dispose() { _transaction?.Dispose(); _context.Dispose(); } } ``` ## Configuratie van Dependency Injection De registratie van services gebeurt in elke laag via extension methods en wordt vervolgens georchestreerd in Program.cs. ```csharp // Infrastructure/DependencyInjection.cs using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Infrastructure.Persistence; using CleanArchitecture.Infrastructure.Repositories; namespace CleanArchitecture.Infrastructure; public static class DependencyInjection { public static IServiceCollection AddInfrastructure( this IServiceCollection services, IConfiguration configuration) { // Configuratie van Entity Framework services.AddDbContext(options => options.UseSqlServer( configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName))); // Unit of Work registreren (Scoped) services.AddScoped(); // Individuele repositories indien nodig services.AddScoped(); services.AddScoped(); return services; } } ``` ```csharp // Application/DependencyInjection.cs using FluentValidation; using MediatR; using Microsoft.Extensions.DependencyInjection; using CleanArchitecture.Application.Common.Behaviors; namespace CleanArchitecture.Application; public static class DependencyInjection { public static IServiceCollection AddApplication(this IServiceCollection services) { var assembly = typeof(DependencyInjection).Assembly; // MediatR registreren met handlers services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(assembly)); // FluentValidation-validators registreren services.AddValidatorsFromAssembly(assembly); // Pipeline behaviors (volgorde van uitvoering is belangrijk) services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); return services; } } ``` ```csharp // Api/Program.cs using CleanArchitecture.Application; using CleanArchitecture.Infrastructure; var builder = WebApplication.CreateBuilder(args); // Lagen registreren builder.Services.AddApplication(); builder.Services.AddInfrastructure(builder.Configuration); // API-configuratie builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); ``` ## De Presentation-laag: API-controllers Controllers zijn eenvoudige adapters die het werk delegeren aan MediatR. ```csharp // Api/Controllers/OrdersController.cs using MediatR; using Microsoft.AspNetCore.Mvc; using CleanArchitecture.Application.Orders.Commands; using CleanArchitecture.Application.Orders.Queries; namespace CleanArchitecture.Api.Controllers; [ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly IMediator _mediator; public OrdersController(IMediator mediator) { _mediator = mediator; } [HttpPost] [ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task Create( [FromBody] CreateOrderCommand command, CancellationToken cancellationToken) { // Volledige delegatie aan MediatR var orderId = await _mediator.Send(command, cancellationToken); return CreatedAtAction( nameof(GetById), new { id = orderId }, orderId); } [HttpGet("{id:guid}")] [ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetById( Guid id, CancellationToken cancellationToken) { var order = await _mediator.Send( new GetOrderByIdQuery(id), cancellationToken); if (order == null) return NotFound(); return Ok(order); } [HttpPost("{id:guid}/confirm")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Confirm( Guid id, CancellationToken cancellationToken) { await _mediator.Send(new ConfirmOrderCommand(id), cancellationToken); return NoContent(); } [HttpPost("{id:guid}/ship")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Ship( Guid id, CancellationToken cancellationToken) { await _mediator.Send(new ShipOrderCommand(id), cancellationToken); return NoContent(); } } ``` Controllers blijven dun en bevatten geen bedrijfslogica, enkel HTTP-mapping. ## Unit tests van de Application-laag Clean Architecture vergemakkelijkt unit tests dankzij de isolatie van afhankelijkheden. ```csharp // Tests/Application/CreateOrderCommandHandlerTests.cs using Moq; using Xunit; using CleanArchitecture.Application.Orders.Commands; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Interfaces; namespace CleanArchitecture.Tests.Application; public class CreateOrderCommandHandlerTests { private readonly Mock _unitOfWorkMock; private readonly CreateOrderCommandHandler _handler; public CreateOrderCommandHandlerTests() { _unitOfWorkMock = new Mock(); _handler = new CreateOrderCommandHandler(_unitOfWorkMock.Object); } [Fact] public async Task Handle_ValidCommand_CreatesOrderAndReturnsId() { // Arrange var product = new Product { Id = Guid.NewGuid(), Name = "Test", Price = 100 }; _unitOfWorkMock.Setup(x => x.Products.GetByIdAsync( It.IsAny(), It.IsAny())) .ReturnsAsync(product); _unitOfWorkMock.Setup(x => x.Orders.AddAsync( It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); _unitOfWorkMock.Setup(x => x.SaveChangesAsync(It.IsAny())) .ReturnsAsync(1); var command = new CreateOrderCommand( CustomerEmail: "test@example.com", Items: new List { new(ProductId: product.Id, Quantity: 2) }); // Act var result = await _handler.Handle(command, CancellationToken.None); // Assert Assert.NotEqual(Guid.Empty, result); _unitOfWorkMock.Verify(x => x.Orders.AddAsync( It.Is(o => o.CustomerEmail == "test@example.com"), It.IsAny()), Times.Once); _unitOfWorkMock.Verify(x => x.SaveChangesAsync( It.IsAny()), Times.Once); } [Fact] public async Task Handle_ProductNotFound_ThrowsNotFoundException() { // Arrange _unitOfWorkMock.Setup(x => x.Products.GetByIdAsync( It.IsAny(), It.IsAny())) .ReturnsAsync((Product?)null); var command = new CreateOrderCommand( CustomerEmail: "test@example.com", Items: new List { new(ProductId: Guid.NewGuid(), Quantity: 1) }); // Act & Assert await Assert.ThrowsAsync( () => _handler.Handle(command, CancellationToken.None)); } } ``` Met Moq-mocks kan de handler worden geïsoleerd van zijn afhankelijkheden, zodat enkel de orkestratielogica wordt getest. ## Conclusie Clean Architecture met .NET levert een robuuste structuur voor enterprise-applicaties. De strikte scheiding van verantwoordelijkheden tussen de Domain-, Application-, Infrastructure- en Presentation-lagen zorgt op lange termijn voor onderhoudbare en testbare code. ### Checklist Clean Architecture .NET - ✅ Geïsoleerde Domain zonder externe afhankelijkheden - ✅ Entiteiten met ingekapselde bedrijfslogica - ✅ Repository-interfaces in de Domain - ✅ Use Cases via Commands/Queries (CQRS) - ✅ Validatie met FluentValidation en Pipeline Behaviors - ✅ Infrastructure implementeert de Domain-interfaces - ✅ Dunne controllers die delegeren aan MediatR - ✅ Geïsoleerde unit tests met mocks De initiële investering in deze architectuur betaalt zich snel terug in middelgrote en grote projecten, waar wijzigingen in technische vereisten (databasemigratie, frameworkwissel) enkel de buitenste lagen raken en de bedrijfskern intact laten. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/dotnet/clean-architecture-dotnet-practical-guide