Sostieni AppuntiFacili con una piccola donazione su PayPal
Dona con PayPalLa Dependency Injection (DI) è un pattern che realizza il principio di Inversion of Control (IoC): una classe non costruisce da sola ciò di cui ha bisogno, ma riceve le dipendenze dall’esterno.
In altre parole, il codice applicativo smette di dire:
e inizia a dire:
Questo “qualcun altro” può essere:
Vantaggi principali:
graph LR
A[Classe client] -->|dipende da| B[Astrazione]
C[Container / Composition Root] -->|fornisce implementazione| A
D[Classe concreta] -->|implementa| B
Inversion of Control significa che il flusso di creazione e collegamento degli oggetti non è più controllato dalle singole classi, ma da un livello superiore.
Senza IoC:
Con IoC:
Dal punto di vista architetturale:
Il punto chiave è che i moduli importanti del dominio non dovrebbero dipendere da dettagli infrastrutturali.
Un modo intuitivo per capire IoC è il Hollywood Principle:
“Don’t call us, we’ll call you.”
Tradotto nel contesto DI:
Esempio mentale:
ILogger, DbContext o servizi applicativi;Questa inversione del controllo rende il codice meno “imperativo” nella costruzione e più dichiarativo nelle dipendenze.
La DI è spesso il meccanismo pratico che permette di applicare il DIP - Dependency Inversion Principle.
Il DIP afferma:
Esempio:
OrdineService è modulo ad alto livello;EmailService è un dettaglio infrastrutturale;INotificaService è l’astrazione.Se OrdineService dipende da INotificaService, allora il dominio resta stabile anche se cambiamo il canale di notifica.
La DI da sola non garantisce una buona architettura, ma rende molto più naturale rispettare il DIP.
// SENZA DI: accoppiamento forte
public class OrdineService
{
private readonly EmailService _emailService;
public OrdineService()
{
// La classe crea la dipendenza -> hard-coded
_emailService = new EmailService();
}
public void ConfermaOrdine(int ordineId)
{
// logica ordine...
_emailService.InviaEmail("Ordine confermato");
}
}
Problemi:
OrdineService conosce una classe concreta.EmailService.Quando una classe usa spesso new per creare collaboratori, sta mescolando due responsabilità:
Un errore comune è pensare: “va bene, non uso new, allora chiedo tutto a un contenitore globale”.
public class OrdineService
{
public void ConfermaOrdine(int ordineId)
{
var notificaService = ServiceLocator.Current.GetRequiredService<INotificaService>();
notificaService.Invia("Ordine confermato");
}
}
Questo approccio è chiamato Service Locator ed è considerato un anti-pattern.
Perché?
Nasconde le dipendenze
Dal costruttore non si capisce più di cosa ha bisogno la classe.
Riduce la leggibilità
Le dipendenze diventano implicite e disperse nel codice.
Rende i test più difficili
Serve spesso configurare un contenitore globale o simulare un resolver.
Viola il principio di explicit dependencies
Una buona API dovrebbe dichiarare chiaramente le collaborazioni necessarie.
Introduce coupling al container
La classe dipende dal meccanismo di risoluzione, non solo dall’astrazione.
Con la DI corretta:
graph TD
A[Service Locator] --> B[La classe cerca i servizi]
B --> C[Dipendenze nascoste]
C --> D[Test e manutenzione peggiori]
E[Dependency Injection] --> F[La classe dichiara le dipendenze]
F --> G[Dipendenze esplicite]
G --> H[Test e manutenzione migliori]
// Interfaccia -> astrazione
public interface INotificaService
{
void Invia(string messaggio);
}
// Implementazione concreta
public class EmailService : INotificaService
{
public void Invia(string messaggio)
=> Console.WriteLine($"Email inviata: {messaggio}");
}
// CON DI: dipende dall'interfaccia, non dall'implementazione
public class OrdineService
{
private readonly INotificaService _notificaService;
// La dipendenza viene INIETTATA nel costruttore
public OrdineService(INotificaService notificaService)
{
_notificaService = notificaService;
}
public void ConfermaOrdine(int ordineId)
{
// logica ordine...
_notificaService.Invia("Ordine confermato");
}
}
Qui la classe:
È la forma più raccomandata perché:
public class MioServizio
{
private readonly IRepository _repo;
public MioServizio(IRepository repo)
{
_repo = repo;
}
}
Se una classe ha troppi parametri nel costruttore, spesso non è un limite della DI ma un segnale architetturale: probabilmente la classe ha troppe responsabilità.
public class MioServizio
{
public ILogger Logger { get; set; } = NullLogger.Instance;
}
È meno sicura perché:
Va bene solo in scenari molto specifici.
public class MioServizio
{
public void Esegui(ILogger logger)
{
logger.Log("Eseguito");
}
}
È utile quando la dipendenza serve solo per una singola operazione, non per tutta la vita dell’oggetto.
IServiceCollection, IServiceProvider e object graph.NET include un contenitore DI nativo minimalista ma molto efficace, basato su:
IServiceCollection: descrive come registrare i servizi;IServiceProvider: sa come risolverli;IServiceScope: delimita uno scope di vita per i servizi scoped.// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IOrdineService, OrdineService>();
builder.Services.AddTransient<IEmailService, EmailService>();
builder.Services.AddSingleton<IConfigService, ConfigService>();
var app = builder.Build();
Il flusso tipico è:
builder.Services.Build(), il framework crea il provider radice.IServiceProvider risolve il grafo degli oggettiSupponiamo:
UserController dipende da IUserService;UserService dipende da IUserRepository e ILogger<UserService>;UserRepository dipende da AppDbContext.Quando il framework deve creare UserController, il container:
Dispose() a fine scope, se necessario.graph TD
A[Request HTTP] --> B[IServiceScope]
B --> C[UserController]
C --> D[IUserService -> UserService]
D --> E[IUserRepository -> UserRepository]
D --> F[ILogger<UserService>]
E --> G[AppDbContext]
In termini concettuali:
Il container attraversa il grafo fino alle foglie, poi costruisce gli oggetti dal basso verso l’alto.
Il provider radice vive per tutta l’applicazione. Da esso vengono creati gli scope:
Scoped non dovrebbero essere risolti direttamente dal provider radice.using var scope = app.Services.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IMioServizio>();
Il container può intercettare alcuni problemi:
Questa validazione è preziosa perché sposta molti errori dalla produzione all’avvio o alla prima risoluzione.
Il lifetime determina per quanto tempo vive un’istanza.
graph TD
A[Request 1] -->|usa| B[Scoped S1]
A -->|usa| C[Transient T1]
A -->|usa| D[Singleton SG]
E[Request 2] -->|usa| F[Scoped S2]
E -->|usa| G[Transient T2]
E -->|usa| D
Nuova istanza ogni volta che il servizio viene richiesto.
builder.Services.AddTransient<IMioServizio, MioServizio>();
Usalo per:
Rischio:
Una sola istanza per scope. In ASP.NET Core di solito significa una richiesta HTTP.
builder.Services.AddScoped<IMioServizio, MioServizio>();
È ideale per:
DbContext;Una sola istanza per tutta la vita dell’applicazione.
builder.Services.AddSingleton<IMioServizio, MioServizio>();
È adatto a:
Un singleton deve essere progettato con attenzione:
| Lifetime | Istanze create | Durata | Uso tipico |
|---|---|---|---|
| Transient | Ogni richiesta al container | Brevissima | Mapper, strategy stateless |
| Scoped | Una per scope | Durata request/scope | DbContext, servizi applicativi request-bound |
| Singleton | Una sola | Vita dell’app | Cache, provider condivisi |
Un errore classico: un singleton che trattiene uno scoped.
public class MioSingleton
{
private readonly IMioScoped _scoped;
public MioSingleton(IMioScoped scoped)
{
_scoped = scoped;
}
}
// builder.Services.AddSingleton<MioSingleton>();
// builder.Services.AddScoped<IMioScoped, MioScoped>();
Perché è sbagliato?
In sviluppo, .NET può lanciare InvalidOperationException quando rileva questo pattern.
Non sempre un servizio si costruisce bene con il semplice mapping interfaccia-implementazione. In questi casi puoi registrarlo con una factory:
builder.Services.AddTransient<ReportService>(sp =>
{
var logger = sp.GetRequiredService<ILogger<ReportService>>();
var clock = sp.GetRequiredService<ISystemClock>();
var culture = CultureInfo.GetCultureInfo("it-IT");
return new ReportService(logger, clock, culture);
});
Questa tecnica è utile quando:
Va usata con disciplina: se la factory diventa molto lunga, probabilmente la composizione andrebbe spostata in una classe dedicata.
Il container .NET supporta la registrazione di generici aperti.
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
Questo significa che:
IRepository<User>, il container crea Repository<User>;IRepository<Order>, crea Repository<Order>.È molto utile per repository, validator, handler, mapper e pipeline generiche.
public interface IRepository<T>
{
T? GetById(int id);
}
public class Repository<T> : IRepository<T>
{
public T? GetById(int id) => default;
}
Con .NET 8 puoi registrare più implementazioni della stessa interfaccia usando una chiave.
builder.Services.AddKeyedScoped<IPagamentoService, CartaPagamentoService>("carta");
builder.Services.AddKeyedScoped<IPagamentoService, PaypalPagamentoService>("paypal");
builder.Services.AddKeyedScoped<IPagamentoService, BonificoPagamentoService>("bonifico");
Risoluzione in un controller:
[ApiController]
[Route("api/pagamenti")]
public class PagamentiController : ControllerBase
{
[HttpPost("carta")]
public IActionResult PagaConCarta(
[FromKeyedServices("carta")] IPagamentoService pagamentoService)
{
pagamentoService.Paga(100m);
return Ok();
}
}
Oppure manualmente:
var pagamento = serviceProvider.GetRequiredKeyedService<IPagamentoService>("paypal");
Quando usarli:
switch o if annidati su tipi concreti.Attenzione: i keyed services non devono diventare un pretesto per reintrodurre logica di selezione caotica nel dominio.
IOptions<T>, IOptionsMonitor<T>, IOptionsSnapshot<T>In .NET la configurazione si integra con la DI tramite l’Options Pattern.
Classe di configurazione:
public class MailOptions
{
public string Host { get; set; } = "";
public int Port { get; set; }
public string Sender { get; set; } = "";
}
Registrazione:
builder.Services.Configure<MailOptions>(
builder.Configuration.GetSection("Mail"));
Uso con IOptions<T>:
public class MailService
{
private readonly MailOptions _options;
public MailService(IOptions<MailOptions> options)
{
_options = options.Value;
}
}
Differenze fondamentali:
| Tipo | Lifetime tipico | Aggiornamenti config | Scenario |
|---|---|---|---|
IOptions<T> | Singleton-like wrapper | No refresh per richiesta | Config quasi statica |
IOptionsSnapshot<T> | Scoped | Valori ricalcolati per scope/request | Web app con config riletta a ogni request |
IOptionsMonitor<T> | Singleton | Supporta change notification e accesso al valore corrente | Background service, singleton, hot reload config |
IOptions<T>IOptionsSnapshot<T>IOptionsMonitor<T>CurrentValue;OnChange(...).public class CacheWarmupService
{
private readonly IOptionsMonitor<MailOptions> _optionsMonitor;
public CacheWarmupService(IOptionsMonitor<MailOptions> optionsMonitor)
{
_optionsMonitor = optionsMonitor;
}
public void StampaHostCorrente()
{
Console.WriteLine(_optionsMonitor.CurrentValue.Host);
}
}
È buona pratica affiancare anche:
Validate(...);ValidateOnStart() per fallire subito se la configurazione è errata.Il Decorator permette di avvolgere un servizio con un altro che implementa la stessa interfaccia, aggiungendo comportamento senza modificare il servizio originale.
public interface IReportService
{
Task GeneraAsync();
}
public class ReportService : IReportService
{
public Task GeneraAsync() => Task.CompletedTask;
}
public class LoggingReportDecorator : IReportService
{
private readonly IReportService _inner;
private readonly ILogger<LoggingReportDecorator> _logger;
public LoggingReportDecorator(
IReportService inner,
ILogger<LoggingReportDecorator> logger)
{
_inner = inner;
_logger = logger;
}
public async Task GeneraAsync()
{
_logger.LogInformation("Inizio generazione report");
await _inner.GeneraAsync();
_logger.LogInformation("Fine generazione report");
}
}
Concettualmente:
graph LR
A[Client] --> B[IReportService]
B --> C[LoggingDecorator]
C --> D[ReportService]
Con il container built-in, il decorator puro non è comodo quanto in container più avanzati. Per questo spesso si usa Scrutor, che aggiunge assembly scanning e decorator registration:
builder.Services.AddScoped<IReportService, ReportService>();
builder.Services.Decorate<IReportService, LoggingReportDecorator>();
La libreria Scrutor estende Microsoft.Extensions.DependencyInjection e consente:
Esempio:
builder.Services.Scan(scan => scan
.FromAssemblyOf<IUserService>()
.AddClasses(classes => classes.InNamespaces("MyApp.Services"))
.AsImplementedInterfaces()
.WithScopedLifetime());
È utile in progetti grandi, ma va usata con moderazione: le registrazioni troppo implicite possono rendere meno chiaro il composition root.
Una dipendenza circolare avviene quando:
A dipende da B;B dipende da C;C dipende da A.graph LR
A[ServiceA] --> B[ServiceB]
B --> C[ServiceC]
C --> A
Esempio:
public class ServiceA
{
public ServiceA(ServiceB b) { }
}
public class ServiceB
{
public ServiceB(ServiceA a) { }
}
Il container in genere le rileva durante la risoluzione e lancia un’eccezione.
Come evitarle:
La soluzione corretta quasi mai è “forzare” il container: di solito bisogna migliorare il design.
La DI rende i test molto più facili perché puoi sostituire le implementazioni reali con finte implementazioni o mock.
Esempio con fake:
public class FakeNotificaService : INotificaService
{
public string? UltimoMessaggio { get; private set; }
public void Invia(string messaggio)
{
UltimoMessaggio = messaggio;
}
}
Test con Microsoft.Extensions.DependencyInjection:
var services = new ServiceCollection();
services.AddScoped<INotificaService, FakeNotificaService>();
services.AddScoped<OrdineService>();
using var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
var ordineService = scope.ServiceProvider.GetRequiredService<OrdineService>();
var fake = (FakeNotificaService)scope.ServiceProvider.GetRequiredService<INotificaService>();
ordineService.ConfermaOrdine(10);
Assert.Equal("Ordine confermato", fake.UltimoMessaggio);
Con un framework di mocking puoi registrare direttamente un mock dell’interfaccia:
services.AddSingleton<INotificaService>(mock.Object);
Buone pratiche nei test:
Nei test di integrazione puoi costruire un container quasi reale e sostituire solo alcune dipendenze:
ISystemClock finto.Questo approccio consente di verificare non solo la logica, ma anche il wiring dell’applicazione.
Microsoft.Extensions.DependencyInjectionPunti forti:
Limiti:
Punti forti:
Contro:
Punti forti:
Contro:
// Interfacce
public interface IUserRepository
{
User? GetById(int id);
}
public interface IUserService
{
string GetNomeUtente(int id);
}
public interface IAuditService
{
void Traccia(string messaggio);
}
// Implementazioni
public class UserRepository : IUserRepository
{
public User? GetById(int id)
{
// simulazione DB
return new User { Id = id, Nome = "Mario" };
}
}
public class ConsoleAuditService : IAuditService
{
public void Traccia(string messaggio)
{
Console.WriteLine($"AUDIT: {messaggio}");
}
}
public class UserService : IUserService
{
private readonly IUserRepository _userRepo;
private readonly IAuditService _auditService;
public UserService(IUserRepository userRepo, IAuditService auditService)
{
_userRepo = userRepo;
_auditService = auditService;
}
public string GetNomeUtente(int id)
{
_auditService.Traccia($"Richiesto utente {id}");
var user = _userRepo.GetById(id);
return user?.Nome ?? "Sconosciuto";
}
}
public class User
{
public int Id { get; set; }
public string Nome { get; set; } = "";
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddSingleton<IAuditService, ConsoleAuditService>();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
// Controller
[ApiController]
[Route("api/users")]
public class UserController : ControllerBase
{
private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var nome = _userService.GetNomeUtente(id);
return Ok(nome);
}
}
In questo esempio:
IUserService;UserService dipende da due astrazioni;Program.cs.Cosa significa Dependency Injection?
Quale tipo di iniezione è generalmente raccomandato?
Un servizio Transient viene creato:
Un servizio Scoped in ASP.NET Core vive:
AddSingleton registra un servizio che:
Cosa si intende per 'captive dependency'?
Dipendere da un'interfaccia piuttosto che da una classe concreta segue quale principio SOLID?
In .NET, dove si registrano tipicamente i servizi?
Quale lifetime è più adatto per un DbContext in ASP.NET Core?
Qual è il principale vantaggio della DI per i test?
Scenario: Vuoi un sistema di notifiche che supporti Email, SMS e Push.
Consegna:
INotificaService con metodo Invia(string messaggio).EmailNotifica, SmsNotifica, PushNotifica.NotificaManager che accetta INotificaService via constructor injection.IServiceCollection e risolvi NotificaManager tramite il container.Obiettivo: capire il pattern DI e come scambiare le implementazioni senza modificare il codice client.
Scenario: Vuoi separare la logica di accesso ai dati dalla logica di business.
Consegna:
IProductRepository con metodi GetAll() e GetById(int id).InMemoryProductRepository che usa una lista in memoria.ProductService che usa IProductRepository via constructor injection.IServiceCollection e testa la risoluzione.Obiettivo: praticare il Repository Pattern combinato con DI.
Scenario: Vuoi verificare visivamente la differenza tra i lifetime.
Consegna:
TransientService, ScopedService, SingletonService, ognuna con un Guid generato nel costruttore.Obiettivo: capire concretamente la differenza tra Transient, Scoped e Singleton.
Prenota una lezione