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.
Ottenimento condizionale di istanze
Spessissimo vi Γ¨ la necessitΓ di ottenere delle istanze diverse in base a determinate condizioni, per esempio qualcosa come:
if (condizione)
return istanzaA;
else
return istanzaB;
Assumendo che sia istanzaA
che istanzaB
siano entrambe correttamente registrate nella DI, come faccio a ottenere una rispetto allβaltra usando la DI?
Ovviamente entrambe devono ereditare dalla stessa interfaccia, nellβesempio sotto IHandler
.
Poi ci sono vari metodi, un trucco Γ¨ usare una classe Orchestrator
che si memorizza un dizionario <string, Type>
con string
la stringa che identifica la condizione dellβif
.
Potrebbe essere un enum
o qualsiasi cosa.
Per popolare tale dizionario sfruttare un Attribute
custom in modo da poter fare la scanning dellβassembly e registrare nella DI in modo automatico senza dover far tutto a mano.
E infine sfruttare questo dizionario per ottenere il servizio richiesto.
Di seguito la spiegazione passo passo con il codice.
1. Definizione dellβInterfaccia e dellβAttributo Personalizzato
Definiamo unβinterfaccia comune e un attributo che verrΓ usato per mappare un comando a una specifica implementazione.
// Interfaccia comune per tutti gli handler.
public interface IHandler
{
Task HandleAsync();
}
// Attributo per associare un nome di comando ad un handler.
[AttributeUsage(AttributeTargets.Class)]
public class CommandNameAttribute : Attribute
{
public string CommandName { get; }
public CommandNameAttribute(string commandName)
{
CommandName = commandName;
}
}
2. Implementazioni Concrete
Creiamo due classi concrete che implementano IHandler
e che vengono identificate da un attributo CommandNameAttribute
.
[CommandName("weather")]
public class WeatherHandler : IHandler
{
public async Task HandleAsync()
{
// Simulazione di una chiamata asincrona ad un servizio esterno.
await Task.Delay(100);
Console.WriteLine("Esecuzione dell'handler per il comando 'weather'.");
}
}
[CommandName("time")]
public class TimeHandler : IHandler
{
public Task HandleAsync()
{
Console.WriteLine("Esecuzione dell'handler per il comando 'time'.");
return Task.CompletedTask;
}
}
3. Handler Orchestrator con Dependency Injection
Lβorchestratore Γ¨ il componente che, dato un comando (una stringa), determina quale handler risolvere dal container DI. In questo esempio, usiamo il container per ottenere lβistanza corretta in base al tipo registrato.
public class HandlerOrchestrator
{
private readonly Dictionary<string, Type> _handlerTypes = new();
private readonly IServiceProvider _serviceProvider;
// Il service provider viene iniettato tramite DI.
public HandlerOrchestrator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
RegisterHandlers();
}
// Scansione dell'assembly per ottenere tutte le implementazioni di IHandler
// e mappare il comando definito dall'attributo.
private void RegisterHandlers()
{
var handlerTypes = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => typeof(IHandler).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
foreach (var type in handlerTypes)
{
var attribute = type.GetCustomAttribute<CommandNameAttribute>();
if (attribute != null)
{
_handlerTypes[attribute.CommandName] = type;
}
}
}
// Risolve l'istanza dell'handler basandosi sul comando.
public IHandler? GetHandler(string command)
{
if (_handlerTypes.TryGetValue(command, out var handlerType))
{
// Otteniamo l'istanza tramite il container DI
return (IHandler)_serviceProvider.GetRequiredService(handlerType);
}
return null;
}
}
4. Configurazione del Container DI e Utilizzo
Dove voglio ottenere lβistanza a partire da un parametro in ingresso basta scrivere qualcosa come.
var orchestrator = serviceProvider.GetRequiredService<HandlerOrchestrator>();
string command = "weather"; // generico parametro in ingresso per ottenere un handler condizionale
var handler = orchestrator.GetHandler(command);
if (handler != null)
await handler.HandleAsync();
Tips & Tricks
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.
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.
Impementare decoration
Assumiamo di volere misurare quanto tempo ci impiega una mia classe a fare una chiamata API, per esempio a ottenere il tempo mediante la classe OpenWeatherService
.
Lβidea Γ¨ creare una classe che wrappa OpenWeatherService
in modo da circondare la sua chiamata API con uno Stopwatch
e misurarne così il tempo senza sporcare la classe OpenWeatherService
.
Per farlo posso sfruttare la DI in questo modo: creo una classe LoggedWeatherService
che riceve in ingresso un IWeatherService
e che a sua volta Γ¨ un IWeatherService
.
public class LoggedWeatherService : IWeatherService
{
private readonly IWeatherService _weatherService; //<-- OpenWeatherService
private readonly ILogger<IWeatherService> _logger;
public LoggedWeatherService(IWeatherService weatherService,
ILogger<IWeatherService> logger)
{
_weatherService = weatherService;
_logger = logger;
}
public async Task<WeatherResponse?> GetCurrentWeatherAsync(string city)
{
var sw = Stopwatch.StartNew();
try
{
return await _weatherService.GetCurrentWeatherAsync(city);
}
finally
{
sw.Stop();
_logger.LogInformation("Weather retrieval for city: {0}, took {1}ms",
city, sw.ElapsedMilliseconds);
}
}
}
e quando definisco tali classi nella ServiceCollection
scrivo:
// Definisco OpenWeatherService as itself
builder.Services.AddTransient<OpenWeatherService>();
// Quando qualcuno chiede un IWeatherService forniscimelo wrappato nel LoggedWeatherService (per questo questo ultimo eredita da IWeatherService).
builder.Services.AddTransient<IWeatherService>(provider =>
new LoggedWeatherService(provider.GetRequiredService<OpenWeatherService>(),
provider.GetRequiredService<ILogger<IWeatherService>>()));
Di fatto quindi scrivo βa manoβ come risolvere IWeatherService
in modo da poterlo wrappare.
Un modo piΓΉ pulito di scrivere questo Γ¨ usare il metodo Decorate
del pacchetto nuget Scrutor
in questo modo
builder.Services.AddTransient<IWeatherService, OpenWeatherService>();
builder.Services.Decorate<IWeatherService, LoggedWeatherService>();
Scrutor
Scrutor Γ¨ un pacchetto nuget che aggiunge delle extension a ServiceCollection
in modo da poter fare delle operazioni aggiuntive.
Esempio sono il Decorate
visto sopra ma sopratutto lo scanning, quindi registrare le dipendenze in maniera automatica facendo lo scan dei tipi definiti in un assembly secondo regole specifiche.
Per esempio sotto aggiungo alla DI come Singleton tutti i tipi nellβassembly che contiene Program
che hanno lβattributo [Singleton]
e così anche per gli altri scope.
services.Scan(selector =>
{
selector
.FromAssemblyOf<Program>()
.AddClasses(f => f.WithAttribute<SingletonAttribute>())
.AsImplementedInterfaces()
.WithSingletonLifetime()
.AddClasses(f => f.WithAttribute<TransientAttribute>())
// Se il servizio giΓ esiste throw exception (raccomandato)
.UsingRegistrationStrategy(RegistrationStrategy.Throw)
.AsImplementedInterfaces()
.WithTransientLifetime()
.AddClasses(f => f.WithAttribute<ScopedAttribute>())
.AsImplementedInterfaces()
.WithScopedLifetime();
});
Questo Γ¨ solo un esempio ma rende lβidea di quando utilizzare Scrutor
rispetto allβaggiunta manuale con il classico pacchetto nuget di Microsoft.
Posso filtrare per:
- Interface marking
- Attribute marking
- Namespace
- Class name (per esempio
EndsWith
) - β¦ Ovviamente lo scanning maschera moltissima logica e rende il codice sicuramente piΓΉ conciso ma anche molto piΓΉ difficile da capire e puΓ² portare a dei bug difficili da scoprire: lo scanning Γ¨ quindi da usare con cautela.