Dometrain

Introduzione

Un potente meccanismo per separare la costruzione degli oggetti dal loro uso รจ la Dependency Injection (DI): lโ€™idea รจ che un oggetto non dovrebbe mai assumersi la responsabilitร  dellโ€™istanziazione delle sue dipendenze, al contrario dovrebbe passare questa responsabilitร  ad un meccanismo piรน โ€œautorevoleโ€, invertendo pertanto il controllo. Usando la DI una classe non intraprende alcun passo diretto per risolvere le sue dipendenze (non chiama per esempio metodi come BuildObject per intenderci) ma รจ completamente passiva: mette a disposizione dei parametri a costruttore o dei metodi Setter e assume che questi parametri vengano popolati dal DI Framework nel modo corretto. Un nome alternativo ma che significa la stessa cosa รจ โ€œInversion of Control Containerโ€ o โ€œIoC Containerโ€.

Separazione di Main

Un modo per separare la costruzione dellโ€™architettura dal suo utilizzo consiste semplicemente di trasferire tutti gli aspetti della costruzione degli oggetti in main o in moduli richiamati da main e nel progettare il resto del sistema supponendo che tutti gli oggetti siano giร  stati costruiti e collegati tra di loro in modo appropriato. Lโ€™obiettivo รจ che la funzione main costruisce gli oggetti necessari per il sistema e poi li passa allโ€™applicazione che li utilizza. In questo modo lโ€™applicazione non ha alcuna conoscenza del main o del processo di costruzione degli oggetti. Ovviamente non tutti gli oggetti possono essere creati subito, in questo caso posso utilizzare il pattern Abstract Factory per dare allโ€™applicazione il controllo sul momento in cui costruire gli oggetti che gli servono ma mantenendo tutti i dettagli di tale costruzione separati rispetto al codice dellโ€™applicazione.

.NET

In .NET esiste il pacchetto nuget ufficiale Microsoft.Extensions.DependencyInjection che permette di iniettare automaticamente le dipendenze necessarie alle classi. Il suo funzionamento base รจ il seguente:

public class Application  
{  
    public Application(IWeatherService weatherService){} 
}
 
var services = new ServiceCollection();  
  
services.AddSingleton<IWeatherService, OpenWeatherService>();  
  
var serviceProvider = services.BuildServiceProvider();  
  
var application = serviceProvider.GetRequiredService<Application>();

Viene creata una ServiceCollection che รจ letteralmente una lista di istruzioni (in particolare List<ServiceDescriptor>) che indicano come devono essere risolte le dipendenze dal DI Framework con il suo lifetime (Transient, Scoped, Singleton) e su di essa viene costruito un ServiceProvider che, come dice il nome, รจ un servizio che fornisce le dipendenze a chi le richiede. La ServiceCollection รจ come un libro di ricette mentre il ServiceProvider รจ il cuoco che le mette insieme per creare la ricetta.

Utilizzando poi i metodi di ServiceProvider posso ottenere lโ€™istanza della classe che voglio, nellโ€™esempio sopra Application. Se questa ultima ha delle dipendenze a costruttore queste vengono automaticamente iniettate dal DI Framework risolvendole: nellโ€™esempio sopra Application ha come dipendenza un IWeatherService che risolve in un OpenWeatherService: verrร  automaticamente iniettato questo ultimo.

ServiceCollection

Lifetime

Ho tre tipologie di lifetime degli oggetti che posso creare:

  • Transient: Ogni volta che richiedo una dipendenza verrร  creata una nuova istanza della classe. Tipicamente piรน sicuro a livello di thread-safety ma piรน lento di singleton.
  • Singleton: Una sola istanza in tutta lโ€™applicazione, verrร  istanziata la prima volta che serve e poi tale istanza verrร  riutilizzata in tutta lโ€™applicazione. Va bene per classi stateless. Eโ€™ lโ€™approccio piรน veloce ma bisogna stare attenti alla thread safety.
  • Scoped: lโ€™oggetto rimane sempre lo stesso allโ€™interno dello stesso scope il quale puรฒ essere definito a mano in caso di una applicazione standalone mentre nel caso di ASP.NET questo รจ per definizione dallโ€™inizio alla fine di una richiesta.
Custom Scope

Se sono in una Console Application e voglio creare uno scope custom posso scrivere come segue:

var serviceScopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();  
  
using (var serviceScope = serviceScopeFactory.CreateScope())  
{  
    var exampleService1 = serviceScope.ServiceProvider.GetRequiredService<ExampleService>();  
    Console.WriteLine(exampleService1.Id);  
}

Sintassi

La sintassi builder.Services.AddTransient|Singleton|Scoped<IService, ConcreteService>(); funziona se tutto lโ€™albero delle dipendenze dei costruttori di ConcreteService puรฒ essere risolto dalla DI: per esempio se ConcreteService dipende da IHttpClientFactory significa che deve essere esplicitato come risolvere tale interfaccia nella ServiceCollection e cosรฌ a ritroso ad albero per tutte le dipendenze. Una sintassi alternativa รจ builder.Services.AddTransient<IWeatherService>(provider => new OpenWeatherService(Not_DI_Class)); da utilizzare quando voglio esplicitare io come costruire un oggetto senza delegarlo alla DI: in questo modo non serve che tutte le sue dipendenze siano definite nella DI in quanto le espliciterรฒ nella lambda factory.

La sintassi da utilizzare รจ inoltre sempre quella โ€œautomaticaโ€ (vedi esempio sotto); la sintassi โ€œmanualeโ€ รจ il suo analogo ma fatto a mano dal programmatore, cosa che difficilmente si fa a meno di casi particolari. Lo scopo dellโ€™esempio sotto รจ dimostrare un minimo cosa viene fatto dietro le quinte quando viene chiamato un metodo della ServiceCollection.

// sintassi automatica
builder.Services.AddTransient<IWeatherService, OpenWeatherService>();
// sintassi manuale
builder.Services.AddTransient<IWeatherService>(provider =>
    new OpenWeatherService(provider.GetRequiredService<IHttpClientFactory>()));
 
// sintassi automatica
builder.Services.AddScoped<LifetimeIndicatorFilter>();
// sintassi manuale
builder.Services.AddScoped(provider =>
{
    // Costruisco con la DI le dipendenze che servono alla classe LifetimeIndicatorFilter
    var idGenerator = provider.GetRequiredService<IdGenerator>();
    var logger = provider.GetRequiredService<ILogger<LifetimeIndicatorFilter>>();
 
    return new LifetimeIndicatorFilter(idGenerator, logger);
});
 

ServiceDescriptor

Ogni elemento allโ€™interno di una ServiceCollection รจ un ServiceDescriptor che, come dice il nome, รจ una classe che descrive come un servizio deve essere istanziato. In particolare quando scrivo, per esempio

builder.Services.AddTransient<IWeatherService, OpenWeatherService>();

Sto creando un ServiceDescriptor cosรฌ:

var openWeatherServiceDescriptor =  
    new ServiceDescriptor(typeof(IWeatherService), typeof(OpenWeatherService), ServiceLifetime.Transient);
builder.Services.Add(openWeatherServiceDescriptor);

Questo strumento piรน a basso livello permette di customizzare lโ€™IoC Container con cose custom fighe come Interceptors, Decorators e cosรฌ via.

Note

Devo rendere tutto interfaccia?

Anche se uno un DI Framework ciรฒ non significa che devo convertire ogni cosa in una interfaccia: tutti gli oggetti che non possono essere sostituiti by definition devono rimanere classi classiche. Anche se tecnicamente potrei iniettarle tramite interfacce a costruttore questo non ha senso.

Tips & Tricks

Testare ILogger

Lโ€™interfaccia ILogger di .NET (vedi Logging in .NET) รจ piena di extension methods ed estremamente difficile da testare. Un trucco รจ creare una classe Adapter che faccia da proxy alle chiamate al log: tale classe puรฒ essere quindi iniettata e testata. Esempio con solo il metodo LogInformation:

public interface ILoggerAdapter<TType>  
{  
    void LogInformation(string template, params object[] args);  
}
 
public class LoggerAdapter<TType> : ILoggerAdapter<TType>  
{  
    private readonly ILogger<LoggerAdapter<TType>> _logger;  
  
    public LoggerAdapter(ILogger<LoggerAdapter<TType>> logger)  
    {        _logger = logger;  
    }  
    public void LogInformation(string template, params object[] args)  
    {        _logger.LogInformation(template, args);  
    }
}

Registrare Open Generics

Dato che non รจ possibile utilizzare gli Open Generic nei tipi per limiti del linguaggio C# posso utilizzare la seguente sintassi sfruttando il typeof.

builder.Services.AddTransient(typeof(ILoggerAdapter<>), typeof(LoggerAdapter<>));

Il codice sopra indica che tutte le volte che richiedo unโ€™interfaccia di tipo ILoggerAdapter, indipendentemente dal suo tipo <T> deve essere istanziato un new LoggerAdapter<T>.

Registrare piรน implementazioni

Se dichiaro piรน implementazioni della stessa interfaccia la seconda non sovrascrive la prima ma, se a costruttore ho IWeatherService fornirรฒ sempre la seconda dichiarata (come se fosse sovrascritta), mentre se ho IEnumerable<IWeatherService> verranno fornire entrambe.

services.AddTransient<IWeatherService, OpenWeatherService>();  
services.AddTransient<IWeatherService, InMemoryWeatherService>();

Se invece voglio che, se una interfaccia era giร  definita non aggiungerne una nuova devo usare il metodo TryAdd invece di Add. Quindi se scrivo

services.TryAddTransient<IWeatherService, OpenWeatherService>();  
services.TryAddTransient<IWeatherService, InMemoryWeatherService>();

Verrร  aggiunto solo OpenWeatherService.

Raggruppare le definizioni in extension

Per evitare di avere il Main con mille righe di definizioni di dipendenze (AddSingleton, AddSingleton, AddTransientโ€ฆ) รจ buona norma raggruppare tutte le istruzioni che riguardano lo stesso metodo in un unico extension method con nome AddXXX. Per esempio posso creare questa extension:

public static IServiceCollection AddEndpointsApiExplorer(this IServiceCollection services)
{
    services.TryAddSingleton<IActionDescriptorCollectionProvider, DefaultActionDescriptorCollectionProvider>();
    services.TryAddSingleton<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>();
    return services;
}

che poi verrร  chiamata dallโ€™esterno in questo modo:

services.AddEndpointsApiExplorer();

Inoltre รจ buona norma (sopratutto per chi fornisce librerie come pacchetti nuget) modificare il namespace di tale classe con

// ReSharper disable once CheckNamespace  
namespace Microsoft.Extensions.DependencyInjection;

in modo tale che dallโ€™esterno non debbia aggiungere uno using dedicato a tale extension.