Primary Constructor e Dependency Injection in C# 12: vantaggi, insidie e quando usarli
#tech
spcnet.it/primary-constructor-…
@informatica
Primary Constructor e Dependency Injection in C# 12: vantaggi, insidie e quando usarli
Con C# 12, Microsoft ha introdotto i primary constructors per tutte le classi e le struct — non solo per i record come in precedenza. Per chi lavora quotidianamente con ASP.NET Core e il pattern di dependency injection, questa feature merita attenzione: riduce il boilerplate in modo significativo, ma nasconde un'insidia che è bene conoscere prima di adottarla sistematicamente.Il problema: il boilerplate del costruttore classico
Chi ha scritto servizi ASP.NET Core sa bene come appare il costruttore tradizionale con dependency injection:public class OrderService { private readonly IOrderRepository _orderRepository; private readonly ILogger<OrderService> _logger; private readonly IEventBus _eventBus; private readonly IValidator<Order> _validator; public OrderService( IOrderRepository orderRepository, ILogger<OrderService> logger, IEventBus eventBus, IValidator<Order> validator) { _orderRepository = orderRepository; _logger = logger; _eventBus = eventBus; _validator = validator; } }
Per ogni dipendenza: una dichiarazione di campo, un parametro nel costruttore, un'assegnazione nel corpo. Con quattro dipendenze, sono già dodici righe di puro scaffolding. In un servizio con sei o sette dipendenze, il costruttore diventa il blocco di codice più lungo della classe — senza contenere logica.La soluzione con primary constructors
Con i primary constructors di C# 12, lo stesso servizio si scrive così:public class OrderService( IOrderRepository orderRepository, ILogger<OrderService> logger, IEventBus eventBus, IValidator<Order> validator) { public async Task<Order?> GetOrderAsync(Guid id) { logger.LogInformation("Fetching order {OrderId}", id); return await orderRepository.GetByIdAsync(id); } public async Task PlaceOrderAsync(Order order) { await validator.ValidateAndThrowAsync(order); await orderRepository.SaveAsync(order); await eventBus.PublishAsync(new OrderPlacedEvent(order.Id)); } }
I parametri del primary constructor diventano direttamente disponibili in tutta la classe senza dichiarazioni esplicite di campo. Il risultato è codice più snello, con meno rumore visivo e il focus immediato sulla logica di business.L'insidia della mutabilità: perché Milan Jovanović era inizialmente scettico
Il motivo di resistenza di molti sviluppatori esperti è legittimo: i parametri del primary constructor non sono campireadonly. Il compilatore non impedisce la riassegnazione accidentale:public class OrderService(IOrderRepository orderRepository) { public void SomeMethod() { orderRepository = null!; // Compila senza warning } }
Con i campi privati readonly tradizionali, questo codice causa un errore di compilazione. Con i primary constructors, il compilatore lo accetta silenziosamente. In un servizio DI dove le dipendenze non dovrebbero mai essere rimpiazzate a runtime, questo è un rischio concreto in team di grandi dimensioni.Mitigazione: assegnazione esplicita a readonly field
Quando la garanzia di immutabilità è critica, si può assegnare esplicitamente il parametro a un campo readonly:public class CriticalService(IRepository repository) { private readonly IRepository _repository = repository; // Da qui in poi si usa _repository, mai repository direttamente }
Questo reintroduce parte del boilerplate, ma mantiene la sintassi più compatta per la firma del costruttore e garantisce l'immutabilità a livello compilatore.Quando usare i primary constructors
La valutazione pragmatica è che i primary constructors offrono il massimo vantaggio nelle classi di servizio DI tipiche di ASP.NET Core, dove i parametri vengono usati ma raramente riassegnati. In questi scenari, il rischio di mutabilità accidentale è basso e i benefici di leggibilità sono immediati.Vale la pena usarli anche per entity e value object dove i parametri di costruzione diventano proprietà read-only:
public class Order(Guid customerId, Money total) { public Guid Id { get; } = Guid.NewGuid(); public Guid CustomerId { get; } = customerId; public Money Total { get; } = total; public DateTime CreatedAt { get; } = DateTime.UtcNow; }Quando evitarli
Tre scenari in cui i primary constructors non sono la scelta giusta:
- Logica di validazione nel costruttore: se il costruttore deve eseguire guard clause o validazioni prima dell'assegnazione, il corpo esplicito del costruttore tradizionale è necessario.
- Multiple signature di costruttore: i primary constructors supportano una sola firma. Con overload multipli (es. per serializzazione), la sintassi tradizionale è l'unica opzione.
- Cinque o più dipendenze: se una classe richiede molte dipendenze, il problema non è la sintassi del costruttore ma il design della classe. Il segnale che suggerisce un refactoring verso interfacce più coese o il pattern Facade.
Conclusione: adottarli con consapevolezza
I primary constructors di C# 12 non sono una rivoluzione, ma un'evoluzione pragmatica del linguaggio. Per la maggior parte delle classi di servizio in ASP.NET Core, il tradeoff è favorevole: meno boilerplate, codice più leggibile, rischio di mutabilità basso nel contesto DI. L'importante è conoscere il comportamento del compilatore e scegliere consapevolmente quando la garanzia direadonlyè effettivamente necessaria.Come sempre con le feature di C#, il consiglio è adottarle dove migliorano la leggibilità del codice reale, non per seguire una moda sintattica.
Fonte originale: Why I Switched to Primary Constructors for DI in C# di Milan Jovanović.
Why I Switched to Primary Constructors for DI in C#
I resisted primary constructors for a while. They felt like a shortcut that would cost me later. But after using them across several projects, I'm sold, with one important caveat.www.milanjovanovic.tech
reshared this
The Pirate Post, The Privacy Post, Cybersecurity & cyberwarfare e Poliversity - Università ricerca e giornalismo reshared this.

Wish the sun to stand still 🌞
in reply to securityaffairs • • •