loading experience

.NET

Dependency Injection in .NET: lifetime e keyed services

Singleton, scoped e transient senza errori, e come registrare più implementazioni della stessa interfaccia con una chiave.

Dependency Injection in .NET: lifetime e keyed services

Il contenitore di dependency injection integrato in .NET copre la grande maggioranza dei casi. Conoscerne bene i tempi di vita e le funzioni più recenti evita bug difficili da trovare.

Un servizio non deve mai dipendere da uno con vita più breve della sua.
Un servizio non deve mai dipendere da uno con vita più breve della sua.

I tre lifetime

  • Singleton: un'unica istanza per tutta l'applicazione. Deve essere thread-safe.
  • Scoped: un'istanza per richiesta web (o per scope creato a mano).
  • Transient: una nuova istanza ogni volta che viene richiesta.

L'errore classico è iniettare un servizio scoped in un singleton: il singleton lo trattiene per sempre, condividendo per esempio un contesto di database tra richieste diverse. In ambiente di sviluppo .NET lo segnala all'avvio.

Keyed services

Quando servono più implementazioni della stessa interfaccia, si registrano con una chiave:

builder.Services.AddKeyedSingleton<INotificatore, NotificatoreEmail>("email");
builder.Services.AddKeyedSingleton<INotificatore, NotificatoreSms>("sms");

public class AvvisiService(
    [FromKeyedServices("email")] INotificatore email,
    [FromKeyedServices("sms")] INotificatore sms)
{
    public Task UrgenteAsync(string testo) =>
        Task.WhenAll(email.InviaAsync(testo), sms.InviaAsync(testo));
}

Nelle Minimal API lo stesso attributo si usa sui parametri dell'handler.

Servizi in background e scope

Un BackgroundService è un singleton. Per usare servizi scoped, come un repository, si crea uno scope esplicito:

public class PuliziaService(IServiceScopeFactory scopes) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
        while (await timer.WaitForNextTickAsync(ct))
        {
            using var scope = scopes.CreateScope();
            var repo = scope.ServiceProvider.GetRequiredService<ILogRepository>();
            await repo.EliminaVecchiAsync(ct);
        }
    }
}

Regola pratica: registra come singleton solo ciò che è davvero condivisibile e senza stato per utente, e lascia scoped tutto ciò che tocca dati della richiesta.

Commenti (0)

Nessun commento ancora.

Lascia un commento