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.