Dependency Injection avanzata in .NET con IServiceProvider e child containers

Dependency Injection avanzata in .NET: strategie con IServiceProvider e child containers

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 comuneEffetto collateraleSoluzione
Uso eccessivo di IServiceProvider.GetService()Difficile da testareUsa DI costruttore o factory
Registrazioni duplicateRisoluzioni imprevedibiliConsolidare IServiceCollection in un punto
Lifetime errato (es. Scoped → Singleton)Memory leak o oggetti condivisiComprendere bene i cicli di vita
Cache di provider localiRiferimenti pendentiEliminare 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

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.

Pubblicato

in

da

Tag: