Dependency Injection: smetti di saldare i tuoi oggetti, usa il carrello degli attrezzi
Scritto da Marco Morello il 4 Febbraio 2026
💡 TL;DR: Il
newall'interno delle classi è una "saldatura" che impedisce la manutenzione. La Dependency Injection è il "bullone" che ti permette di cambiare componenti senza rompere il sistema. In questo articolo esploriamo Lifetimes, Container e Best Practices in .NET.
Nel post precedente abbiamo parlato di Composition over Inheritance usando l'analogia dei LEGO. Abbiamo capito che è meglio assemblare piccoli componenti specializzati piuttosto che creare gerarchie rigide. Ma una volta che abbiamo tutti questi pezzi (servizi, logger, database), sorge un problema meccanico fondamentale: chi ha la responsabilità di assemblarli?
Se nel tuo codice scrivi new MioServizio() dentro una classe, stai effettuando quella che io chiamo una saldatura strutturale. In meccanica, se saldi un componente al telaio invece di usare dadi e bulloni, rendi la manutenzione un incubo: per cambiare un pezzo devi tagliare, smerigliare e risaldare. In informatica, il new dentro una classe crea un accoppiamento forte (Tight Coupling) che rende il codice rigido, fragile e impossibile da testare seriamente.
Oggi entriamo nel profondo della Dependency Injection (DI). Non vedremo solo come usarla, ma capiremo perché è il sistema nervoso centrale di ogni applicazione .NET moderna, da una complessa Web API a un'app MAUI per la gestione dell'officina.
1. La genesi filosofica: la trinità del controllo
Per padroneggiare la DI, dobbiamo prima fare pulizia terminologica. Molti sviluppatori usano IoC, DIP e DI come sinonimi, ma sono concetti posti su livelli diversi di astrazione. Capire questa gerarchia è ciò che distingue un programmatore che "copia codice" da un architetto del software.
A. Inversion of Control (IoC) - Il cambio di paradigma
L'IoC è il concetto più ampio. Normalmente, il tuo codice è il "pilota": decide quando frenare, quando curvare e quando cambiare marcia. In un sistema IoC, tu scrivi solo la logica dei singoli componenti e deleghi a un "pilota automatico" (il Framework) il compito di decidere quando chiamarli.
- Mantra: "Don't call us, we'll call you" (Il principio di Hollywood).
- Analogia meccanica: in un'officina tradizionale, il meccanico deve ricordarsi di accendere il compressore, aprire l'aria e poi usare la pistola pneumatica. In un'officina "IoC", appena il meccanico solleva la pistola dal supporto, il compressore parte e l'aria arriva automaticamente. Il controllo della logistica è invertito: il meccanico si concentra solo sul bullone.
B. Dependency Inversion Principle (DIP) - La regola architetturale
È la "D" dei principi SOLID. Martin Fowler e Robert C. Martin sottolineano due regole:
- I moduli di alto livello (la tua logica di business, es. "Calcola Margine Officina") non devono dipendere da quelli di basso livello (es. "Salva su SQLite"). Entrambi devono dipendere da astrazioni.
- Le astrazioni non devono dipendere dai dettagli. I dettagli (le implementazioni) devono dipendere dalle astrazioni.
Perché è vitale? Se la tua logica di business dipende direttamente da SqliteDatabase, la tua logica è "ostaggio" della tecnologia. Se il database fallisce o cambia, la tua logica deve essere riscritta. Se dipendi da un'interfaccia IDataStore, la tua logica è pura, protetta e pronta a correre su qualsiasi strada, che sia sterrata o asfaltata come la SS45.
C. Dependency Injection (DI) - Lo strumento pratico
La DI è il pattern specifico che realizza l'IoC per quanto riguarda la gestione delle dipendenze. È l'atto fisico di fornire a un oggetto le risorse di cui ha bisogno tramite un fornitore esterno (il Container), invece di fargliele creare internamente.
2. Anatomia del problema: il costo nascosto della "saldatura"
Analizziamo cosa succede realmente quando scriviamo new in un metodo.
public class ManutenzioneService {
public void EseguiTagliando() {
// SALDATURA: Accoppiamento forte e rigido
var logger = new FileLogger(@"C:\logs\officina.txt");
var database = new SqliteDatabase();
logger.Info("Inizio tagliando...");
database.Salva("Intervento su Yamaha XT1200Z");
}
}
Questo frammento nasconde tre "mine anti-uomo":
- Rigidità (Open/Closed violation): se vuoi inviare i log a un database in cloud (es. Azure Application Insights), devi modificare, ricompilare e ridistribuire
ManutenzioneService. - Propagazione dell'errore (Fragilità): se il costruttore di
FileLoggercambia (magari richiede un parametro extra), tutte le classi che lo istanziano smettono di compilare. È l'effetto domino che ti tiene sveglio la notte. - Mancanza di isolamento: non puoi testare questa logica senza avere un file fisico o un database reale. I tuoi Unit Test diventano lenti, sporchi e dipendenti dall'ambiente.
3. Il motore di .NET: IServiceCollection e il grafo delle dipendenze
Quando avvii un'app .NET, il framework crea un Container (il IServiceProvider). Immaginalo come un magazziniere con un registro di montaggio millimetrico che tiene traccia di come costruire ogni singolo pezzo della tua moto.
La registrazione (il catalogo)
Nel Program.cs, compili questa lista usando IServiceCollection.
var builder = WebApplication.CreateBuilder(args);
// Associazioni tra Contratto (Interface) e Implementazione (Classe)
builder.Services.AddScoped<IRepositoryMoto, SqlServerRepository>();
builder.Services.AddSingleton<IEmailService, SendGridEmailService>();
La risoluzione (il runtime)
Quando chiedi al Container un oggetto, avviene la Dependency Graph Resolution.
Se chiedi un Controller, il Container vede che serve ServiceA, il quale richiede RepositoryB, il quale richiede DbContext. Il Container risale l'intera catena (il grafo delle dipendenze), crea tutti gli oggetti nell'ordine corretto (dai più profondi ai più superficiali) e te li consegna già assemblati. Se un pezzo del puzzle manca, l'app "esplode" al boot invece che crashare in modo casuale mentre l'utente la usa: un vantaggio enorme per la stabilità.
4. Lifetimes: gestione della memoria e pericoli degli IDisposable
Questa è la parte dove si decidono le performance dell'app. Sbagliare il Lifetime non crea solo bug logici, ma può saturare la RAM o corrompere i dati tra diversi utenti.
A. Transient (AddTransient) - L'elettrodo
Viene creata una nuova istanza ogni singola volta che viene richiesta.
- Uso: Servizi leggeri, senza stato (stateless). Come un elettrodo della saldatrice: lo usi per quel punto e poi lo getti.
- ⚠️ Attenzione agli IDisposable: Se registri un servizio
IDisposablecome Transient, il container terrà un riferimento ad esso fino alla chiusura dello "scope". Se lo risolvi migliaia di volte in un ciclo, potresti causare un memory leak temporaneo ma massiccio perché il container non può liberarli finché lo scope non finisce.
B. Scoped (AddScoped) - La scheda di officina
Viene creata una sola volta per ogni "Scope" (tipicamente una richiesta HTTP).
- Importante: È il lifetime obbligatorio per il
DbContextdi Entity Framework. Garantisce che tutti i repository che lavorano sulla stessa richiesta usino la stessa connessione, permettendo le transazioni atomiche (o tutto o niente).
C. Singleton (AddSingleton) - Il compressore imbullonato
Viene creata una sola volta e vive finché l'app non viene spenta.
- Pericolo Thread-Safety: Poiché un Singleton è condiviso da tutti i thread (e quindi da tutti gli utenti) contemporaneamente, i suoi metodi devono essere "Thread-Safe". Mai usare liste o variabili globali senza protezioni (come i
locko leConcurrentCollections).
D. La trappola mortale: captive dependency
Si verifica quando un oggetto con un lifetime lungo (Singleton) dipende da uno con un lifetime breve (Scoped).
Esempio: Immagina il Capo Officina (Singleton) che prende la Scheda di Lavoro di un cliente (Scoped). Se trattiene quella scheda per sempre, finirà per annotare i lavori di tutti i clienti futuri su quel vecchio foglio ormai chiuso, mischiando i dati. In .NET, questo porta spesso a errori di "DbContext disposed" o leak di memoria. .NET moderno lancia un'eccezione in modalità Development se rileva questa situazione proprio per salvarti la pelle.
5. Configurazione professionale: options pattern
Non iniettare mai stringhe di connessione o chiavi API "nude" nei tuoi servizi. Questo accoppia il tuo codice al file di configurazione. La soluzione professionale è l'Options Pattern, che trasforma il tuo JSON in oggetti C# validati e pronti all'uso.
// Classe per le impostazioni
public class OfficinaSettings {
public string ConnectionString { get; set; }
public int MaxRiparazioniContemporanee { get; set; }
}
// Nel Program.cs
builder.Services.Configure<OfficinaSettings>(builder.Configuration.GetSection("Officina"));
// Nel servizio
public class CalcoloService(IOptions<OfficinaSettings> options) {
private readonly OfficinaSettings _settings = options.Value;
}
IOptions<T>: Singleton, legge i dati una volta.IOptionsSnapshot<T>: Scoped, utile se vuoi cambiare le impostazioni nel file JSON senza riavviare il server.IOptionsMonitor<T>: Singleton, ma aggiorna i valori in tempo reale con eventi di notifica.
6. Gestione avanzata: scopes manuali e keyed services
IServiceScopeFactory (Il "garage temporaneo")
A volte ti trovi in un servizio Singleton (come un Background Worker che gira ogni ora) e hai bisogno di un servizio Scoped (come il database). Non puoi iniettarlo direttamente (causeresti una Captive Dependency). Devi creare uno scope manuale:
public class MyWorker(IServiceScopeFactory scopeFactory) : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
using (var scope = scopeFactory.CreateScope()) {
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
// Ora puoi usare il DB in sicurezza e verrà chiuso correttamente al termine del using
}
}
}
Keyed Services (.NET 8+)
Cosa succede se hai due implementazioni di IStorage (uno locale e uno cloud)? Fino a .NET 7 era complesso gestirli. Con .NET 8 puoi usare una chiave:
builder.Services.AddKeyedSingleton<IStorage, LocalStorage>("locale");
builder.Services.AddKeyedSingleton<IStorage, AzureStorage>("cloud");
// Iniezione mirata tramite attributo nel costruttore
public class BackupService([FromKeyedServices("cloud")] IStorage backupDrive) { ... }
7. Performance e database: AddDbContextPool
Nelle applicazioni ad alto traffico, creare migliaia di istanze di DbContext al secondo è un carico pesante per il Garbage Collector. EF Core offre il pooling: invece di distruggere l'oggetto, il container lo "pulisce" e lo rimette in una coda per riutilizzarlo.
- Usa
AddDbContextPool<MyDbContext>(...)per ridurre l'overhead di allocazione. - ⚠️ Nota: Non usarlo se il tuo DbContext mantiene uno stato interno specifico per l'utente che deve essere resettato manualmente.
8. Unit testing: perché la DI è la tua polizza assicurativa
Il vero potere della DI si vede quando devi garantire che il tuo codice funzioni senza "sporcarti le mani" con database reali.
Mocking vs stubbing
Quando inietti una dipendenza per un test, usi dei simulatori:
- Stub: Un oggetto finto che restituisce sempre lo stesso valore (es. un repository che dice "ho trovato 0 moto").
- Mock: Un oggetto intelligente (usando librerie come
Moq) che verifica se i metodi sono stati chiamati correttamente con i parametri giusti.
[Fact]
public void Esegui_DovrebbeSalvareDati_SenzaToccareIlDatabaseReale() {
var mockRepo = new Mock<IRepositoryMoto>();
var service = new ManutenzioneService(mockRepo.Object);
service.Esegui("Tagliando 20.000km");
// Verifica: Il servizio ha davvero tentato di salvare?
mockRepo.Verify(r => r.SalvaIntervento("Tagliando 20.000km"), Times.Once);
}
9. Errori comuni e "codice che puzza" (code smells)
- Service locator pattern: Se scrivi
_provider.GetService<T>()dentro una funzione, stai imbrogliando. Stai nascondendo le dipendenze sotto il tappeto. Le dipendenze devono essere esplicite nel costruttore per rendere chiaro cosa serve alla classe per "vivere". - Dipendenze circolari: A ha bisogno di B, e B ha bisogno di A. Il sistema va in blocco al boot. È il segnale che le tue classi sono troppo "grasse" e intrecciate.
- Costruttori infiniti: Se vedi 10+ parametri nel costruttore, la classe sta facendo troppo. Torna all'articolo sulla Composition e dividi le responsabilità.
Conclusione: la strada verso l'eccellenza
La Dependency Injection non è un muro da scalare, è l'infrastruttura che permette al tuo software di crescere senza diventare fragile. È ciò che ti permette di cambiare fornitore di database in un pomeriggio o di scrivere migliaia di test che garantiscono che la tua app non crashi mentre l'utente è in officina.
Quando progetterai la tua prossima app (magari quel tool per i margini di finanza), chiediti sempre: "Sto saldando o sto imbullonando?". Se imbulloni, sarai pronto a cambiare qualsiasi pezzo della tua moto senza dover buttare via il telaio.
Per chi vuole approfondire (Fonti Master):
Buon viaggio tra le dipendenze, e ricorda: la pulizia del codice è come la pulizia in officina. Se è tutto in ordine, i problemi si trovano subito.
🔜 Prossimamente su "Il viaggio del programmatore"
Abbiamo disaccoppiato le classi e gestito le dipendenze. Ma cosa succede quando qualcosa va storto?
Nel prossimo articolo parleremo di Error Handling pragmatico: perché il try-catch
non è un estintore e come gestire le eccezioni senza nascondere la polvere sotto il tappeto.