1. Introduzione
La Dependency Injection (DI) è uno dei pilastri architetturali di .NET moderno, ma nella pratica quotidiana molti sviluppatori si fermano ai concetti base: registrare servizi in Program.cs
e iniettarli nel costruttore.
Tuttavia, in progetti complessi — come microservizi, sistemi multi-tenant o flussi dinamici — è necessario padroneggiare aspetti avanzati: la gestione manuale del IServiceProvider
, la creazione di child containers e l’uso di lifetime personalizzati.
In questo articolo esploriamo come sfruttare al massimo il container .NET nativo e come estenderlo in modo pulito e sicuro.
2. Ripasso rapido: IServiceCollection vs IServiceProvider
Il container DI in .NET si basa su due interfacce chiave:
IServiceCollection services = new ServiceCollection();
services.AddTransient<IMailer, SmtpMailer>();
services.AddScoped<IOrderService, OrderService>();
services.AddSingleton<ILogger, ConsoleLogger>();
IServiceProvider provider = services.BuildServiceProvider();
var orderService = provider.GetRequiredService<IOrderService>();
IServiceCollection
: la configurazione — contiene la mappa di registrazioni.IServiceProvider
: il contenitore runtime che risolve le istanze.
Quando crei il provider, .NET costruisce internamente un grafo di dipendenze pronto per risolvere richieste.
Ma cosa succede quando vogliamo creare “provider figli” o container temporanei?
3. Child containers e Service Scope
Il concetto di child container (o scoped container) permette di creare un ambiente isolato per risoluzioni temporanee.
È utilissimo in scenari come:
- richieste web indipendenti,
- background job paralleli,
- esecuzioni multi-tenant,
- o processi con lifetimes differenti.
Ecco un esempio concreto:
using var scope = provider.CreateScope();
var scopedProvider = scope.ServiceProvider;
var orderService = scopedProvider.GetRequiredService<IOrderService>();
In questo modo:
Scoped
→ viene ricreato per ogni scope;Transient
→ istanza sempre nuova;Singleton
→ condiviso fra tutti gli scope.
👉 Ogni scope è un “child container”: eredita le registrazioni globali, ma mantiene le proprie istanze scoped.
4. IServiceProviderFactory e container custom
Per casi avanzati, puoi sostituire il container nativo con uno personalizzato (Autofac, Lamar, SimpleInjector, ecc.) o creare provider custom con IServiceProviderFactory
.
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
Oppure, se vuoi un factory su misura:
public class CustomServiceProviderFactory
: IServiceProviderFactory<IServiceCollection>
{
public IServiceCollection CreateBuilder(IServiceCollection services)
{
// aggiungi logica custom di configurazione
return services;
}
public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder)
{
var provider = containerBuilder.BuildServiceProvider();
// aggiungi wrapper o hook
return provider;
}
}
Questo approccio è ideale se vuoi:
- Intercettare la creazione del container;
- Aggiungere logiche di validazione;
- Integrare container esterni o dinamici;
- Sostituire comportamenti di risoluzione.
5. Scenari pratici con IServiceProvider
🔹 A. Plugin o moduli dinamici
Immagina un sistema che carica estensioni o plugin a runtime, ognuno con le proprie dipendenze.
Puoi creare un container separato per ogni plugin:
var pluginServices = new ServiceCollection();
pluginServices.AddScoped<IPluginService, MyPlugin>();
var pluginProvider = pluginServices.BuildServiceProvider();
var pluginInstance = pluginProvider.GetRequiredService<IPluginService>();
Ogni provider è isolato, ma può ricevere dipendenze comuni dal provider principale se configurato in cascata.
🔹 B. Multi-tenant o contesti utente
In applicazioni SaaS o B2B, ogni tenant può avere servizi o configurazioni specifiche.
Puoi creare uno IServiceScope
per ogni tenant:
public class TenantContainerFactory
{
private readonly IServiceProvider _root;
public TenantContainerFactory(IServiceProvider root)
{
_root = root;
}
public IServiceProvider CreateForTenant(string tenantId)
{
var scope = _root.CreateScope();
var scopedProvider = scope.ServiceProvider;
// eventualmente registrare configurazioni dinamiche
var config = scopedProvider.GetRequiredService<IConfigurationService>();
config.LoadForTenant(tenantId);
return scopedProvider;
}
}
🔹 C. Controllo esplicito dei lifetimes
Puoi creare manualmente istanze gestite tramite provider locale, utile in test, task asincroni o pipeline:
using (var scope = provider.CreateScope())
{
var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();
await processor.ProcessAsync(order);
}
6. Best practice e insidie comuni
Errore comune | Effetto collaterale | Soluzione |
---|---|---|
Uso eccessivo di IServiceProvider.GetService() | Difficile da testare | Usa DI costruttore o factory |
Registrazioni duplicate | Risoluzioni imprevedibili | Consolidare IServiceCollection in un punto |
Lifetime errato (es. Scoped → Singleton) | Memory leak o oggetti condivisi | Comprendere bene i cicli di vita |
Cache di provider locali | Riferimenti pendenti | Eliminare provider/scopes dopo l’uso |
7. Strategie avanzate di risoluzione dinamica
Puoi sfruttare IServiceProvider
anche per risolvere dinamicamente servizi sconosciuti al momento del build:
public class DynamicHandlerFactory
{
private readonly IServiceProvider _provider;
public DynamicHandlerFactory(IServiceProvider provider)
{
_provider = provider;
}
public IHandler Resolve(string handlerType)
{
Type type = Type.GetType(handlerType)!;
return (IHandler)_provider.GetRequiredService(type);
}
}
In questo modo, puoi costruire pipeline dinamiche, sistemi di job orchestration, o plugin framework completamente modulari.
8. Conclusione
La Dependency Injection in .NET è molto più di una “registrazione di servizi”:
è un sistema di orchestrazione del ciclo di vita.
Saper gestire IServiceProvider
, IServiceScope
e i child containers significa costruire applicazioni scalabili, modulari e ottimizzate per il lungo periodo.
Che si tratti di gestire tenant multipli, plugin runtime o job isolati, la comprensione profonda di questi meccanismi permette di trasformare la DI da semplice strumento a motore architetturale.
Fonti e approfondimenti
- Microsoft Docs – Dependency Injection fundamentals
- Steve Gordon – Advanced DI Scenarios in ASP.NET Core
- Andrew Lock – Dependency Injection in .NET
- Autofac Integration Guide
FAQ
1. A cosa serve la Dependency Injection in .NET?
Serve a separare le responsabilità, facilitare i test e migliorare la manutenibilità, risolvendo automaticamente le dipendenze di classi e servizi tramite un container centralizzato.
2. Qual è la differenza tra IServiceCollection e IServiceProvider?IServiceCollection
definisce la registrazione dei servizi, mentre IServiceProvider
rappresenta l’oggetto runtime che risolve le istanze (il container effettivo).
3. Quando usare i child containers in .NET?
Quando hai bisogno di scopi temporanei o scope multipli indipendenti, come per richieste web isolate, processi paralleli, o plugin caricati dinamicamente.
4. Posso creare container separati in un’unica applicazione .NET?
Sì. Puoi generare container figli o provider temporanei con IServiceScopeFactory
o IServiceProvider.CreateScope()
per gestire lifetimes distinti.
5. Quali sono gli errori comuni nella Dependency Injection avanzata?
- Registrare troppi servizi come singleton.
- Dipendere da
IServiceProvider
direttamente in molte classi. - Non comprendere il ciclo di vita di scope e transient.
- Creare memory leak mantenendo riferimenti a provider locali.