Clean Architecture in .NET 2026: CQRS, MediatR e Domande per Colloqui Tecnici

Una guida completa alla Clean Architecture in .NET con CQRS e MediatR. Include best practice, esempi di codice e domande frequenti nei colloqui per architetti .NET.

Clean Architecture in .NET con CQRS e MediatR

La Clean Architecture si è affermata come pattern architetturale di riferimento per le applicazioni .NET moderne. Combinata con CQRS (Command Query Responsibility Segregation) e MediatR, consente di creare sistemi manutenibili, testabili e scalabili. Questo articolo esplora l'implementazione pratica di questi pattern in .NET 9 e prepara ai colloqui tecnici per sviluppatori senior.

La Clean Architecture separa la logica di business dai dettagli tecnici come database e framework. Questa separazione permette test indipendenti e facilita notevolmente i futuri cambi tecnologici.

Che cos'è la Clean Architecture?

La Clean Architecture, conosciuta anche come Onion Architecture o Hexagonal Architecture, organizza il codice in strati concentrici. Lo strato più interno contiene la logica di dominio, mentre gli strati esterni gestiscono infrastruttura e presentazione.

I principi fondamentali includono:

  • Indipendenza dai framework: La logica di business non conosce i framework
  • Testabilità: Le regole di business possono essere testate senza UI, database o servizi esterni
  • Indipendenza dalla UI: L'interfaccia utente può essere sostituita senza modificare le regole di business
  • Indipendenza dal database: Oracle, SQL Server o MongoDB sono intercambiabili

Struttura del Progetto in .NET

Una tipica soluzione Clean Architecture in .NET consiste in quattro progetti principali:

csharp
// Struttura del progetto
MyApp.Domain/           // Entità, Value Objects, Domain Events
MyApp.Application/      // Use Cases, Commands, Queries, Interfaces
MyApp.Infrastructure/   // Accesso ai dati, servizi esterni
MyApp.WebApi/          // Controller, Middleware, configurazione DI

La direzione delle dipendenze punta sempre verso l'interno. Infrastructure e WebApi referenziano Application, ma mai il contrario.

CQRS: Separare Commands e Queries

CQRS separa le operazioni di lettura e scrittura in modelli distinti. I Commands modificano lo stato, le Queries restituiscono dati senza effetti collaterali.

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

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

Configurazione di MediatR

MediatR implementa il pattern Mediator e disaccoppia i mittenti dai destinatari. La configurazione in .NET 9 avviene nel Program.cs:

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

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

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

Pipeline Behaviors per Cross-Cutting Concerns

I Pipeline Behaviors permettono l'implementazione di aspetti trasversali come validazione, logging e 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 con MediatR

I Domain Events segnalano eventi di business importanti e permettono reazioni debolmente accoppiate:

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

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

Repository Pattern e Unit of Work

Il Repository Pattern astrae l'accesso ai dati e rende il dominio indipendente dal layer di persistenza:

csharp
// Interface nell'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);
}

// Implementazione nell'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 con MediatR

.NET 9 permette API Minimal eleganti in combinazione con MediatR:

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

Pronto a superare i tuoi colloqui su .NET?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Unit Testing con Clean Architecture

La separazione in layer semplifica notevolmente i test:

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(
            "Prodotto Test",
            99.99m,
            "Descrizione");

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

Domande Frequenti nei Colloqui sulla Clean Architecture

Domanda 1: Qual è la differenza tra Clean Architecture e architettura N-Tier tradizionale?

Nelle architetture N-Tier, le dipendenze puntano dall'alto verso il basso: Presentation → Business → Data Access. La Clean Architecture inverte questa dipendenza: il Dominio sta al centro e non ha dipendenze verso gli strati esterni. L'Infrastructure implementa interfacce definite nell'Application Layer.

Domanda 2: Quando utilizzare CQRS?

CQRS è adatto per sistemi con requisiti di lettura e scrittura diversi. Casi d'uso tipici includono: elevate richieste di lettura con scritture rare, logica di dominio complessa nelle operazioni di scrittura, o quando servono modelli di lettura diversi per consumatori differenti.

Domanda 3: Come prevenire dipendenze circolari tra i layer?

Attraverso l'applicazione coerente del Dependency Inversion Principle: le interfacce sono definite nel layer interno (Application), le implementazioni nel layer esterno (Infrastructure). La Dependency Injection configura l'associazione a runtime.

Domanda 4: Quali sono gli svantaggi della Clean Architecture?

Maggiore sforzo iniziale, più codice boilerplate e complessità aumentata per progetti piccoli. I vantaggi emergono solo in applicazioni medio-grandi con prospettive di manutenzione a lungo termine.

Domanda 5: Come gestire i Domain Events in sistemi distribuiti?

Per i sistemi distribuiti, MediatR non è sufficiente. Gli Integration Events vengono distribuiti tramite message broker come RabbitMQ, Azure Service Bus o Kafka. L'Outbox Pattern garantisce la consistenza tra operazioni di database e pubblicazione degli eventi.

Best Practice per il 2026

  1. Vertical Slices in combinazione con Clean Architecture: Le feature sono organizzate come unità autonome, mantenendo la separazione in layer all'interno di ogni slice.

  2. Result Pattern invece di Exceptions: Commands e Queries restituiscono oggetti Result che modellano esplicitamente successo o fallimento.

  3. Source Generators per il boilerplate: I .NET Source Generators riducono il codice ripetitivo per mapping e validazioni.

  4. Aspire per lo sviluppo Cloud-Native: .NET Aspire semplifica l'orchestrazione di applicazioni Clean Architecture in ambienti containerizzati.

Conclusione

La Clean Architecture con CQRS e MediatR costituisce una base solida per applicazioni .NET scalabili. La chiara separazione delle responsabilità facilita manutenzione, testing ed evoluzione del software. Per i colloqui tecnici, la comprensione dei principi sottostanti è importante quanto l'implementazione pratica. L'investimento in un'architettura pulita ripaga nel lungo termine attraverso una ridotta complessità e una maggiore produttività degli sviluppatori.

Sfida del giorno

Sapresti trovare il bug in .NET?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 15 settembre 2026

Tag

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

Condividi

Articoli correlati