Recommandations relatives à l’injection de dépendances

Cet article fournit des instructions générales et des meilleures pratiques pour implémenter 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.
  • Limitez la taille des services, faites en sorte qu’elles soient bien factorisées et facilement testées.

Si une classe possède de nombreuses dépendances injectées, il peut s’agir d’un signe que la classe a trop de responsabilités et viole le principe de responsabilité unique (SRP). Essayez 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 sur des instances IDisposable. 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()");
}

L’élément supprimable 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()");
}

L’élément supprimable 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()");
}

L’élément supprimable précédent est destiné à avoir une durée de vie singleton.

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped<ScopedDisposable>();
builder.Services.AddSingleton<SingletonDisposable>();

using IHost host = builder.Build();

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

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

await host.RunAsync();

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

Prenez le code suivant :

// Register example service in IServiceCollection
builder.Services.AddSingleton(new ExampleService());

Dans le code précédent :

  • L’instance ExampleService n’est pas créée par le conteneur de service.
  • L’infrastructure ne supprime pas automatiquement les services.
  • Le développeur est responsable de la suppression des services.

Conseils sur l’interface IDisposable pour les instances temporaires et partagées

Durée de vie temporaire et limitée

Scénario

L’application nécessite une instance IDisposable 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 méthode Create 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 IDisposable partagée 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 limitée. Utilisez IServiceScopeFactory.CreateScope pour créer un nouveau IServiceScope. Utilisez le IServiceProvider de l’étendue pour obtenir les services requis. Supprimez l’étendue quand elle n’est plus nécessaire.

Recommandations générales pour IDisposable

  • N’inscrivez pas d’instances IDisposable avec une durée de vie temporaire. Utilisez plutôt le modèle de fabrique.
  • Ne résolvez pas d’instances IDisposable avec une durée de vie temporaire ou limitée 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 dépendance IDisposable via l’injection des dépendances ne nécessite pas que le récepteur implémente IDisposable lui-même. Le récepteur de la dépendance IDisposable ne doit pas appeler Dispose sur cette dépendance.
  • Utilisez des étendues pour contrôler les durées de vie des services. Les étendues ne sont pas hiérarchiques et il n’existe aucune connexion particulière entre les étendues.

Pour plus d’informations sur le nettoyage des ressources, consultez Implémenter une méthode Disposeou Implémenter une méthode DisposeAsync. En outre, tenez compte du scénario Services temporaires supprimables capturés par conteneur, car il est lié au nettoyage des ressources.

Remplacement de conteneur de services par défaut

Le conteneur de services intégré a été conçu pour répondre aux besoins de l’infrastructure et de la plupart des applications consommatrices. 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, par exemple :

  • Injection de propriétés
  • Injection basée sur le nom (.NET 7 et versions antérieures uniquement. Pour plus d’informations, consultez Services à clé.)
  • 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 des applications ASP.NET Core :

Sécurité des threads

Créez des services singleton thread-safe. Si un service singleton a une dépendance vis-à-vis d’un service temporaire, ce dernier peut également nécessiter la cohérence de thread, en fonction de la manière dont il est utilisé par le singleton.

La méthode de fabrique 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

  • La résolution de service basée sur async/await et Task 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 « conteneurs 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é à utiliser ailleurs.
  • Conservez les fabriques d’injection des dépendances 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 services à é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 d’appeler BuildServiceProvider lors de la configuration des services. Les appareils à BuildServiceProvider se produisent généralement lorsque le développeur souhaite résoudre un service lors de l’inscription d’un autre. De ce fait, utilisez plutôt une surcharge qui inclut IServiceProvider.
  • Les services temporaires supprimables sont capturés par le conteneur à des fins d’élimination. Cela peut se transformer en fuite de mémoire si le service est résolu à partir du conteneur de niveau supérieur.
  • Activez la validation de l’étendue pour vous assurer que l’application n’a pas de singletons qui capturent des services 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 et sont principalement des cas particuliers dans l’infrastructure elle-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 apprennent 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 supprimables capturés par conteneur

Lorsque vous inscrivez des services temporaires qui implémentent IDisposable, par défaut, le conteneur d’injection des dépendances conserve ces références, et ne leur applique pas d’action Dispose() jusqu’à ce que le conteneur soit supprimé lorsque l’application s’arrête s’ils ont été résolus à partir du conteneur, ou jusqu’à ce que l’étendue soit supprimée s’ils ont été résolus à partir d’une étendue. Cela peut se transformer en fuite de mémoire si le service est résolu à partir du niveau du conteneur.

Anti-modèle : jetables temporaires sans suppression. Ne pas copier !

Dans l’anti-modèle précédent, 1 000 objets ExampleDisposable sont instanciés et associés à une racine. 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 Core.

Les fabriques d’injection des dépendances asynchrones peuvent provoquer des interblocages.

Le terme « fabriques d’injection des dépendances » fait référence aux méthodes de surcharge qui existent lors de l’appel de Add{LIFETIME}. Il existe des surcharges acceptant un Func<IServiceProvider, T>T est le service inscrit et le paramètre est nommé implementationFactory. implementationFactory peut être fournie 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 interblocage.

Anti-modèle : interblocage avec fabrique asynchrone. Ne pas copier !

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

Anti-modèle : interblocage avec problème interne de fabrique asynchrone. Ne pas copier !

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é créé 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 contient un service de courte durée captif.

Anti-modèle : dépendance captive. Ne pas copier !

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

namespace DependencyInjection.AntiPatterns;

public class Foo(Bar bar)
{
}

L’objet Foo nécessite un objet Bar, et puisque Foo est un singleton et que Bar est limité, il s’agit d’une configuration incorrecte. En l’état, Foo ne serait instancié qu’une seule fois et conserverait Bar pour sa durée de vie, qui est plus longue que la durée de vie prévue de Bar. Vous devez envisager de valider les étendues, en transmettant validateScopes: true à BuildServiceProvider(IServiceCollection, Boolean). Lorsque vous validez les étendues, vous obtenez un InvalidOperationException avec un message semblable à « 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.

Anti-modèle : le service délimité devient singleton. Ne pas copier !

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 montrer quel exemple de récupération est incorrect.

Voir aussi