Recommandations relatives à l’injection de dépendances

Cet article fournit des instructions générales et des bonnes pratiques pour l’implémentation de l’injection de dépendances dans les applications .NET.

Conception de services pour l’injection de dépendances

Lors de la conception de services pour l’injection de dépendances :

  • Évitez les classes et les membres statiques avec état. Évitez de créer un état global en concevant des applications pour utiliser des services singleton à la place.
  • Éviter une instanciation directe de classes dépendantes au sein de services. L’instanciation directe associe le code à une implémentation particulière.
  • Faites des services petits, bien factornés et facilement testés.

Si une classe a de nombreuses dépendances injectées, cela peut être un signe que la classe a trop de responsabilités et viole le principe de responsabilité unique (SRP). Tentez de refactoriser la classe en déplaçant certaines de ses responsabilités dans de nouvelles classes.

Suppression des services

Le conteneur est responsable du nettoyage des types qu’il crée et appelle Dispose les IDisposable instances. Les services résolus à partir du conteneur ne doivent jamais être supprimés par le développeur. Si un type ou une fabrique est inscrit en tant que singleton, le conteneur supprime automatiquement le singleton.

Dans l’exemple suivant, les services sont créés par le conteneur de service et supprimés automatiquement :

namespace ConsoleDisposable.Example;

public sealed class TransientDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
}

Le jetable précédent est destiné à avoir une durée de vie temporaire.

namespace ConsoleDisposable.Example;

public sealed class ScopedDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}

Le jetable précédent est destiné à avoir une durée de vie limitée.

namespace ConsoleDisposable.Example;

public sealed class SingletonDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}

Le jetable précédent est destiné à avoir une durée de vie singleton.

using ConsoleDisposable.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using IHost host = CreateHostBuilder(args).Build();

ExemplifyDisposableScoping(host.Services, "Scope 1");
Console.WriteLine();

ExemplifyDisposableScoping(host.Services, "Scope 2");
Console.WriteLine();

await host.RunAsync();

static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
    .ConfigureServices((_, services) =>
        services.AddTransient<TransientDisposable>()
    .AddScoped<ScopedDisposable>()
    .AddSingleton<SingletonDisposable>());

static void ExemplifyDisposableScoping(IServiceProvider services, string scope)
{
    Console.WriteLine($"{scope}...");

    using IServiceScope serviceScope = services.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;

    _ = provider.GetRequiredService<TransientDisposable>();
    _ = provider.GetRequiredService<ScopedDisposable>();
    _ = provider.GetRequiredService<SingletonDisposable>();
}

La console de débogage affiche l’exemple de sortie suivant après l’exécution :

Scope 1...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

Scope 2...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

info: Microsoft.Hosting.Lifetime[0]
      Application started.Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
     Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
     Content root path: .\configuration\console-di-disposable\bin\Debug\net5.0
info: Microsoft.Hosting.Lifetime[0]
     Application is shutting down...
SingletonDisposable.Dispose()

Services non créés par le conteneur de services

Considérez le code suivant :

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(new ExampleService());

    // ...
}

Dans le code précédent :

  • L’instance ExampleServicen’est pas créée par le conteneur de service.
  • L’infrastructure ne se débarrasse pas automatiquement des services.
  • Le développeur est responsable de la suppression des services.

Conseils IDisposable pour les instances temporaires et partagées

Durée de vie temporaire et limitée

Scénario

L’application nécessite une IDisposable instance avec une durée de vie temporaire pour l’un des scénarios suivants :

  • L’instance est résolue dans l’étendue racine (conteneur racine).
  • L’instance doit être supprimée avant la fin de l’étendue.

Solution

Utilisez le modèle de fabrique pour créer une instance en dehors de l’étendue parente. Dans ce cas, l’application a généralement une Create méthode qui appelle directement le constructeur du type final. Si le type final a d’autres dépendances, la fabrique peut :

Instance partagée, durée de vie limitée

Scénario

L’application nécessite une instance partagée IDisposable entre plusieurs services, mais l’instance IDisposable doit avoir une durée de vie limitée.

Solution

Inscrivez l’instance avec une durée de vie délimitée. Utilisez IServiceScopeFactory.CreateScope pour créer un nouveau IServiceScope. Utilisez l’étendue pour IServiceProvider obtenir les services requis. Supprimer l’étendue lorsqu’elle n’est plus nécessaire.

Instructions générales IDisposable

  • N’inscrivez IDisposable pas d’instances avec une durée de vie temporaire. Utilisez plutôt le modèle de fabrique.
  • Ne résolvez IDisposable pas les instances avec une durée de vie temporaire ou étendue dans l’étendue racine. La seule exception à cela est si l’application crée/recrée et supprime IServiceProvider, mais ce n’est pas un modèle idéal.
  • La réception d’une IDisposable dépendance par le biais d’une DI ne nécessite pas que le récepteur s’implémente IDisposable lui-même. Le récepteur de la IDisposable dépendance ne doit pas appeler Dispose cette dépendance.
  • Utilisez des étendues pour contrôler la durée de vie des services. Les étendues ne sont pas hiérarchiques et il n’existe aucune connexion spéciale entre les étendues.

Pour plus d’informations sur le nettoyage des ressources, consultez Implémenter une Dispose méthode ou Implémenter une DisposeAsync méthode. En outre, envisagez le scénario Services temporaires jetables capturés par le conteneur en ce qui concerne le nettoyage des ressources.

Remplacement de conteneur de services par défaut

Le conteneur de service intégré est conçu pour répondre aux besoins de l’infrastructure et de la plupart des applications grand public. Nous vous recommandons d’utiliser le conteneur intégré, sauf si vous avez besoin d’une fonctionnalité spécifique qu’il ne prend pas en charge, comme :

  • Injection de propriétés
  • Injection en fonction du nom
  • Conteneurs enfants
  • Gestion personnalisée de la durée de vie
  • Func<T> prend en charge l’initialisation tardive
  • Inscription basée sur une convention

Les conteneurs tiers suivants peuvent être utilisés avec ASP.NET Core applications :

Sécurité des threads

Créez des services singleton thread-safe. Si un service singleton a une dépendance sur un service temporaire, le service temporaire peut également nécessiter une sécurité de thread en fonction de la façon dont il est utilisé par le singleton.

La méthode d’usine d’un service singleton, comme le deuxième argument de AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), n’a pas besoin d’être thread-safe. Comme un constructeur de type (static), il est garanti d’être appelé une seule fois par un seul thread.

Recommandations

  • async/await et Task la résolution de service basée n’est pas prise en charge. Étant donné que C# ne prend pas en charge les constructeurs asynchrones, utilisez des méthodes asynchrones après la résolution synchrone du service.
  • Évitez de stocker des données et des configurations directement dans le conteneur de services. Par exemple, le panier d’achat d’un utilisateur ne doit en général pas être ajouté au conteneur de services. La configuration doit utiliser le modèle d’options. De même, évitez les objets « titulaires de données » qui n’existent que pour autoriser l’accès à un autre objet. Il est préférable de demander l’élément réel par le biais de l’injection de dépendance.
  • Évitez l’accès statique aux services. Par exemple, évitez de capturer IApplicationBuilder.ApplicationServices en tant que champ statique ou propriété pour une utilisation ailleurs.
  • Maintenez les fabriques de di rapides et synchrones.
  • Évitez d’utiliser le modèle de localisateur de service. Par exemple, n’appelez pas GetService pour obtenir une instance de service lorsque vous pouvez utiliser l’injection de dépendance à la place.
  • Une autre variante de localisateur de service à éviter consiste à injecter une fabrique qui résout les dépendances au moment de l’exécution. Ces deux pratiques combinent des stratégies Inversion de contrôle.
  • Évitez les appels à BuildServiceProvider dans ConfigureServices. L’appel BuildServiceProvider se produit généralement lorsque le développeur souhaite résoudre un service dans ConfigureServices.
  • Les services temporaires jetables sont capturés par le conteneur pour être supprimés. Cela peut se transformer en une fuite de mémoire si elle est résolue à partir du conteneur de niveau supérieur.
  • Activez la validation d’étendue pour vous assurer que l’application n’a pas de singletons qui capturent les services délimités. Pour plus d’informations, consultez Validation de l’étendue.

Comme pour toutes les recommandations, vous pouvez vous trouver dans des situations où il est nécessaire d’ignorer une recommandation. Les exceptions sont rares, la plupart du temps des cas particuliers au sein du framework lui-même.

L’injection de dépendance constitue une alternative aux modèles d’accès aux objets statiques/globaux. Il est possible que vous ne bénéficiez pas des avantages de l’injection de dépendances si vous la combinez avec l’accès aux objets statiques.

Exemples d’anti-modèles

En plus des instructions de cet article, il existe plusieurs anti-modèles que vous devez éviter. Certains de ces anti-modèles sont des enseignements tirés du développement des runtimes eux-mêmes.

Avertissement

Il s’agit d’exemples d’anti-modèles, ne copiez pas le code, n’utilisez pas ces modèles et évitez ces modèles à tout prix.

Services temporaires jetables capturés par le conteneur

Lorsque vous inscrivez des services temporaires qui implémentent IDisposable, par défaut, le conteneur DI conserve ces références, et non Dispose() d’entre elles jusqu’à ce que le conteneur soit supprimé lorsque l’application s’arrête si elles ont été résolues à partir du conteneur, ou jusqu’à ce que l’étendue soit supprimée si elles ont été résolues à partir d’une étendue. Cela peut se transformer en une fuite de mémoire si elle est résolue à partir du niveau du conteneur.

static void TransientDisposablesWithoutDispose()
{
    var services = new ServiceCollection();
    services.AddTransient<ExampleDisposable>();
    ServiceProvider serviceProvider = services.BuildServiceProvider();

    for (int i = 0; i < 1000; ++ i)
    {
        _ = serviceProvider.GetRequiredService<ExampleDisposable>();
    }

    // serviceProvider.Dispose();
}

Dans l’anti-modèle précédent, 1 000 ExampleDisposable objets sont instanciés et rootés. Ils ne seront pas supprimés tant que l’instance serviceProvider n’est pas supprimée.

Pour plus d’informations sur le débogage des fuites de mémoire, consultez Déboguer une fuite de mémoire dans .NET.

Les fabriques de di asynchrones peuvent provoquer des interblocages

Le terme « fabriques di » fait référence aux méthodes de surcharge qui existent lors de l’appel Add{LIFETIME}de . Il existe des surcharges qui acceptent un Func<IServiceProvider, T>T est le service en cours d’inscription, et le paramètre est nommé implementationFactory. Peut implementationFactory être fourni en tant qu’expression lambda, fonction locale ou méthode. Si la fabrique est asynchrone et que vous utilisez Task<TResult>.Result, cela entraîne un blocage.

static void DeadLockWithAsyncFactory()
{
    var services = new ServiceCollection();
    services.AddSingleton<Foo>(implementationFactory: provider =>
    {
        Bar bar = GetBarAsync(provider).Result;
        return new Foo(bar);
    });

    services.AddSingleton<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider();
    _ = serviceProvider.GetRequiredService<Foo>();
}

Dans le code précédent, le implementationFactory reçoit une expression lambda où le corps appelle Task<TResult>.Result sur une Task<Bar> méthode de retour. Cela provoque un interblocage. La GetBarAsync méthode émule simplement une opération de travail asynchrone avec Task.Delay, puis appelle GetRequiredService<T>(IServiceProvider).

static async Task<Bar> GetBarAsync(IServiceProvider serviceProvider)
{
    // Emulate asynchronous work operation
    await Task.Delay(1000);

    return serviceProvider.GetRequiredService<Bar>();
}

Pour plus d’informations sur les instructions asynchrones, consultez Programmation asynchrone : informations et conseils importants. Pour plus d’informations sur le débogage des interblocages, consultez Déboguer un interblocage dans .NET.

Lorsque vous exécutez cet anti-modèle et que l’interblocage se produit, vous pouvez afficher les deux threads en attente à partir de la fenêtre Piles parallèles de Visual Studio. Pour plus d’informations, consultez Afficher les threads et les tâches dans la fenêtre Piles parallèles.

Dépendance captive

Le terme « dépendance captive » a été inventé par Mark Seemann et fait référence à la mauvaise configuration des durées de vie des services, où un service de longue durée de vie détient un service captif de courte durée.

static void CaptiveDependency()
{
    var services = new ServiceCollection();
    services.AddSingleton<Foo>();
    services.AddScoped<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider();
    // Enable scope validation
    // using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);

    _ = serviceProvider.GetRequiredService<Foo>();
}

Dans le code précédent, Foo est inscrit en tant que singleton et Bar est délimité, ce qui en surface semble valide. Toutefois, considérez l’implémentation de Foo.

namespace DependencyInjection.AntiPatterns
{
    public class Foo
    {
        public Foo(Bar bar)
        {
        }
    }
}

L’objet Foo nécessite un Bar objet, et puisque Foo est un singleton, et Bar est délimité . Il s’agit d’une configuration incorrecte. Tel quel, Foo ne serait instancié qu’une seule fois, et il conserverait Bar pendant sa durée de vie, qui est plus longue que la durée de vie délimitée prévue de Bar. Vous devez envisager de valider les étendues, en passant validateScopes: true à .BuildServiceProvider(IServiceCollection, Boolean) Lorsque vous validez les étendues, vous obtenez un InvalidOperationException avec un message similaire à « Impossible de consommer le service délimité 'Bar' à partir de singleton 'Foo'. ».

Pour plus d’informations, consultez Validation de l’étendue.

Service délimité en tant que singleton

Lorsque vous utilisez des services délimités, si vous ne créez pas d’étendue ou dans une étendue existante, le service devient un singleton.

static void ScopedServiceBecomesSingleton()
{
    var services = new ServiceCollection();
    services.AddScoped<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);
    using (IServiceScope scope = serviceProvider.CreateScope())
    {
        // Correctly scoped resolution
        Bar correct = scope.ServiceProvider.GetRequiredService<Bar>();
    }

    // Not within a scope, becomes a singleton
    Bar avoid = serviceProvider.GetRequiredService<Bar>();
}

Dans le code précédent, Bar est récupéré dans un IServiceScope, ce qui est correct. L’anti-modèle est la récupération de Bar en dehors de l’étendue, et la variable est nommée avoid pour indiquer l’exemple de récupération incorrect.

Voir aussi