Clean Code Architecture C#: Guida Completa e Domande per Colloqui 2026

Guida completa alla Clean Code Architecture in C# con principi SOLID, architettura a strati e le domande più frequenti nei colloqui tecnici per sviluppatori .NET.

Diagramma Clean Code Architecture C# con layer

La Clean Code Architecture in C# combina i principi del Clean Code di Robert C. Martin con il pattern Clean Architecture per sviluppare applicazioni .NET manutenibili, testabili e scalabili. I colloqui tecnici si concentrano sempre più su questi concetti perché rivelano come un candidato ragiona sulla progettazione del software, andando oltre la semplice funzionalità del codice.

Suggerimento per il Colloquio

Quando viene posta una domanda sulla Clean Architecture, gli intervistatori si aspettano che i candidati spieghino la Dependency Rule: le dipendenze del codice sorgente puntano verso l'interno, verso le policy di livello superiore. Il layer Domain non sa nulla dell'Infrastructure, non il contrario.

Principi SOLID come Fondamento del Clean Code in C#

I principi SOLID costituiscono la base del Clean Code in C#. La documentazione ufficiale Microsoft sui fondamenti .NET raccomanda questi pattern per le applicazioni enterprise. Ogni principio affronta uno specifico problema di manutenibilità.

Single Responsibility Principle (SRP): Una classe ha un solo motivo per cambiare. L'OrderService seguente gestisce esclusivamente l'elaborazione degli ordini, delegando persistenza e notifiche a componenti separati.

OrderService.cscsharp
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)
    {
        // Valida e crea l'entità di dominio
        var order = Order.Create(request.CustomerId, request.Items);
        
        // Persiste attraverso l'astrazione del repository
        await _orderRepository.AddAsync(order);
        
        // Notifica attraverso un servizio separato
        await _notificationService.SendOrderConfirmationAsync(order);
        
        return order;
    }
}

Open/Closed Principle (OCP): Le classi rimangono aperte all'estensione ma chiuse alla modifica. Nuovi metodi di pagamento richiedono nuove classi, non modifiche a quelle esistenti.

IPaymentProcessor.cscsharp
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)
    {
        // Implementazione specifica per Stripe
        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): Le sottoclassi devono essere sostituibili alle loro classi base senza compromettere la correttezza del programma.

csharp
// Implementazione LSP corretta
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): I client non dovrebbero essere costretti a dipendere da interfacce che non utilizzano.

csharp
// Esempio errato - interfaccia grassa
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);
}

// Esempio corretto - interfacce segregate
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): I moduli di alto livello non dovrebbero dipendere dai moduli di basso livello. Entrambi dovrebbero dipendere dalle astrazioni.

csharp
// Domain Layer - definisce l'astrazione
public interface IEmailSender
{
    Task SendAsync(string to, string subject, string body);
}

// Infrastructure Layer - implementa l'astrazione
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));
    }
}

Layer della Clean Architecture nelle Applicazioni C#

La Clean Architecture organizza il codice in strati concentrici con rigide regole di dipendenza. Gli strati più interni contengono la logica di business, mentre quelli esterni gestiscono le preoccupazioni infrastrutturali.

Domain Layer (Strato Più Interno)

Il Domain Layer contiene le regole di business enterprise e le entità. Nessuna dipendenza da framework o librerie esterne.

Domain/Entities/Order.cscsharp
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() { } // Per 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

L'Application Layer contiene i casi d'uso e orchestra il flusso dei dati tra Domain e strati esterni.

Application/UseCases/CreateOrderUseCase.cscsharp
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)
    {
        // Valida la disponibilità dei prodotti
        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}");
        }

        // Crea l'entità di dominio
        var order = Order.Create(command.CustomerId, command.Items);
        
        // Persiste
        await _orderRepository.AddAsync(order);
        await _unitOfWork.SaveChangesAsync();
        
        // Pubblica l'evento di dominio
        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

L'Infrastructure Layer implementa le interfacce definite negli strati interni.

Infrastructure/Persistence/OrderRepository.cscsharp
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 per Clean Architecture

La Dependency Injection permette il disaccoppiamento tra gli strati. ASP.NET Core fornisce un container DI integrato.

Program.cscsharp
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();

Pronto a superare i tuoi colloqui su .NET?

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

Domande Frequenti nei Colloqui sulla Clean Architecture

I colloqui tecnici verificano la comprensione della Clean Architecture attraverso domande concettuali e pratiche.

Domanda: Spiegare la Dependency Rule nella Clean Architecture.

La Dependency Rule stabilisce che le dipendenze del codice sorgente possono puntare solo verso l'interno. Gli strati esterni possono referenziare quelli interni, ma mai il contrario. Il Domain Layer rimane completamente indipendente da framework, database o preoccupazioni della UI.

Domanda: In cosa differisce la Clean Architecture dall'architettura N-Tier?

L'architettura N-Tier organizza il codice in strati orizzontali (Presentation, Business, Data), dove ogni strato dipende da quello sottostante. La Clean Architecture utilizza strati concentrici dove le dipendenze puntano verso l'interno. La differenza chiave è che nella Clean Architecture la logica di business non ha conoscenza dello strato di accesso ai dati.

Domanda: Quando non si dovrebbe usare la Clean Architecture?

La Clean Architecture aggiunge complessità che potrebbe non essere giustificata per semplici applicazioni CRUD o prototipi. Progetti piccoli con ambito limitato potrebbero beneficiare maggiormente di architetture più semplici. L'overhead si giustifica in applicazioni enterprise a lungo termine con requisiti in evoluzione.

Strategie di Testing nella Clean Architecture

La Clean Architecture abilita test completi grazie a confini chiari e Dependency Injection.

csharp
// Unit Test per 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);
    }
}

Gestione degli Errori nella Clean Architecture

Un approccio coerente alla gestione degli errori attraverso tutti gli strati migliora la manutenibilità.

Domain/Exceptions/DomainException.cscsharp
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 }));
    }
}

Conclusione

La Clean Code Architecture in C# offre un approccio strutturato per costruire applicazioni .NET manutenibili. Seguendo i principi SOLID e mantenendo una chiara separazione degli strati, i team possono sviluppare codice facile da testare, estendere e mantenere nel lungo termine. La comprensione di questi concetti prepara non solo ai colloqui tecnici, ma anche alle sfide pratiche dello sviluppo software enterprise.

L'investimento nella Clean Architecture ripaga nei progetti più grandi, dove i requisiti cambiano e i team crescono. La complessità iniziale viene ampiamente compensata dai vantaggi a lungo termine in termini di testabilità, flessibilità e qualità del codice.

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 24 agosto 2026

Tag

#csharp
#clean-architecture
#solid-principles
#dotnet
#colloqui

Condividi

Articoli correlati