Note
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de changer d’annuaire.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de changer d’annuaire.
.NET prend en charge le modèle de conception logicielle d’injection de dépendances (DI), qui est une technique permettant d’obtenir l’inversion de contrôle (IoC) entre les classes et leurs dépendances. L’injection de dépendances dans .NET est une partie intégrée de l’infrastructure, ainsi que la configuration, la journalisation et le modèle d’options.
Une dépendance est un objet dont dépend un autre objet. La classe suivante MessageWriter a une Write méthode dont d’autres classes peuvent dépendre :
public class MessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
Une classe peut créer une instance de la MessageWriter classe pour utiliser sa Write méthode. Dans l’exemple suivant, la MessageWriter classe est une dépendance de la Worker classe :
public class Worker : BackgroundService
{
private readonly MessageWriter _messageWriter = new();
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1_000, stoppingToken);
}
}
}
Dans ce cas, la Worker classe crée et dépend directement de la MessageWriter classe. Les dépendances codées en dur comme celle-ci sont problématiques et doivent être évitées pour les raisons suivantes :
- Pour remplacer
MessageWriterpar une autre implémentation, vous devez modifier laWorkerclasse. - Si
MessageWriterelle a des dépendances, laWorkerclasse doit également les configurer. Dans un grand projet comportant plusieurs classes dépendant deMessageWriter, le code de configuration est disséminé dans l’application. - Cette implémentation complique le test unitaire. L’application doit utiliser une classe
MessageWriterfictive ou stub, ce qui est impossible avec cette approche.
Le concept
L’injection de dépendances résout les problèmes de dépendance codés en dur via :
L’utilisation d’une interface ou classe de base pour extraire l’implémentation des dépendances.
Inscription de la dépendance dans un conteneur de service.
.NET fournit un conteneur de service intégré, IServiceProvider. Les services sont généralement inscrits au démarrage de l’application et ajoutés à un IServiceCollection. Une fois tous les services ajoutés, utilisez-le BuildServiceProvider pour créer le conteneur de services.
Injection du service dans le constructeur de la classe où il est utilisé.
Le framework prend la responsabilité de la création d’une instance de la dépendance et de sa suppression lorsqu’elle n’est plus nécessaire.
Conseil / Astuce
Dans la terminologie d’injection de dépendances, un service est généralement un objet qui fournit un service à d’autres objets, tels que le IMessageWriter service. Le service n’est pas lié à un service web, même s’il peut utiliser un service web.
Par exemple, supposons que l’interface IMessageWriter définit la Write méthode. Cette interface est implémentée par un type concret, MessageWriterillustré précédemment. L’exemple de code suivant inscrit le IMessageWriter service avec le type MessageWriter concret. La méthode AddSingleton inscrit le service avec une durée de vie de singleton , ce qui signifie qu'il n'est pas supprimé tant que l'application n'est pas arrêtée.
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();
using IHost host = builder.Build();
host.Run();
// <SnippetMW>
public class MessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
// </SnippetMW>
// <SnippetIMW>
public interface IMessageWriter
{
void Write(string message);
}
// </SnippetIMW>
// <SnippetWorker>
public sealed class Worker(IMessageWriter messageWriter) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1_000, stoppingToken);
}
}
}
// </SnippetWorker>
Dans l’exemple de code précédent, les lignes en surbrillance :
- Créez une instance de générateur d’applications hôte.
- Configurez les services en inscrivant le
Workerservice hébergé et l’interfaceIMessageWriteren tant que service singleton avec une implémentation correspondante de laMessageWriterclasse. - Générez l’hôte et exécutez-le.
L’hôte contient le fournisseur de services d’injection de dépendances. Il contient également tous les autres services pertinents nécessaires pour instancier automatiquement le Worker et fournir l’implémentation correspondante IMessageWriter en tant qu’argument.
En utilisant le modèle d’interface utilisateur, le service Worker n’utilise pas le type MessageWriterconcret, seule l’interface IMessageWriter qu’il implémente. Cette conception facilite la modification de l’implémentation utilisée par le service Worker sans modifier le service Worker. Le service Worker ne crée pas non plus d’instance de MessageWriter. Le conteneur d’adresses de disponibilité crée l’instance.
À présent, imaginez que vous souhaitez remplacer MessageWriter par un type qui utilise le service de journalisation fourni par le cadre. Créez une classe LoggingMessageWriter qui dépend ILogger<TCategoryName> de celle-ci en la demandant dans le constructeur.
public class LoggingMessageWriter(
ILogger<LoggingMessageWriter> logger) : IMessageWriter
{
public void Write(string message) =>
logger.LogInformation("Info: {Msg}", message);
}
Pour passer à MessageWriter, mettez simplement à jour l’appel de LoggingMessageWriter pour enregistrer cette nouvelle AddSingleton implémentation :
builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();
Conseil / Astuce
Le conteneur résout ILogger<TCategoryName> en tirant parti des types ouverts (génériques), ce qui élimine la nécessité d’enregistrer chaque type construit (générique).
Comportement d’injection de constructeurs
Les services peuvent être résolus à l’aide IServiceProvider (le conteneur de service intégré) ou ActivatorUtilities.
ActivatorUtilities crée des objets qui ne sont pas inscrits dans le conteneur et qui sont utilisés avec certaines fonctionnalités d’infrastructure.
Les constructeurs peuvent accepter des arguments qui ne sont pas fournis par l’injection de dépendances, mais les arguments doivent affecter des valeurs par défaut.
Lors de la résolution des services par IServiceProvider ou ActivatorUtilities, l'injection de constructeur nécessite un constructeur public.
Lorsqu'ActivatorUtilities résout les services, l'injection de constructeur exige qu'un seul constructeur applicable existe. Les surcharges de constructeurs sont prises en charge, mais une seule peut exister dont les arguments peuvent tous être satisfaits par l’injection de dépendances.
Règles de sélection du constructeur
Lorsqu’un type définit plusieurs constructeurs, le fournisseur de services dispose d’une logique pour déterminer le constructeur à utiliser. Le constructeur avec le plus grand nombre de paramètres pour lesquels les types pouvant être résolus par injection de dépendances est sélectionné. Prenons l’exemple de service suivant :
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(ServiceA serviceA, ServiceB serviceB)
{
// ...
}
}
Dans le code précédent, supposons que la journalisation a été ajoutée et est résolvable à partir du fournisseur de services, mais que les types ServiceA et ServiceB ne le sont pas. Le constructeur avec le ILogger<ExampleService> paramètre résout l’instance ExampleService . Même s’il existe un constructeur qui définit plus de paramètres, les types ServiceA et ServiceB ne sont pas résolvables par DI.
En cas d’ambiguïté lors de la découverte des constructeurs, une exception est levée. Considérez l’exemple de service C# suivant :
Avertissement
Ce ExampleService code avec des paramètres de type ambigus pouvant être résolus par DI lève une exception.
Ne faites pas cela : l'objectif est de montrer ce que signifient les types résolvables par DI ambigus.
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(IOptions<ExampleOptions> options)
{
// ...
}
}
Dans l’exemple précédent, il existe trois constructeurs. Le premier constructeur est sans paramètre et ne nécessite aucun service du fournisseur de services. Supposons que la journalisation et les options ont été ajoutées au conteneur d’injections de dépendances et qu’il s’agit de services pouvant être résolus par injection de service. Lorsque le conteneur DI tente de résoudre le type ExampleService, une exception est levée, car les deux constructeurs sont ambiguës.
Évitez l’ambiguïté en définissant un constructeur qui accepte les deux types résolvables DI à la place :
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(
ILogger<ExampleService> logger,
IOptions<ExampleOptions> options)
{
// ...
}
}
Validation de l’étendue
Les services délimités sont supprimés par le conteneur qui les a créés. Si un service délimité est créé dans le conteneur racine, la durée de vie du service est effectivement promue en singleton , car elle n’est supprimée que par le conteneur racine lorsque l’application s’arrête. La validation des étendues du service permet de traiter ces situations quand BuildServiceProvider est appelé.
Lorsqu’une application s’exécute dans l’environnement de développement et appelle CreateApplicationBuilder pour générer l’hôte, le fournisseur de services par défaut effectue des vérifications pour vérifier que :
- Les services délimités ne sont pas résolus à partir du fournisseur de services racine.
- Les services délimités ne sont pas injectés dans les singletons.
Scénarios d’étendue
IServiceScopeFactory est toujours inscrit en tant que singleton, mais IServiceProvider peut varier en fonction de la durée de vie de la classe contenante. Par exemple, si vous résolvez des services à partir d’une étendue et que l’un de ces services prend un IServiceProvider, il s’agit d’une instance délimitée.
Pour définir la portée des services au sein d’implémentations de IHostedService, tels que le BackgroundService, n’injectez pas les dépendances de service via l’injection de constructeur. Au lieu de cela, injectez IServiceScopeFactory, créez une étendue, puis résolvez les dépendances de l’étendue pour utiliser la durée de vie de service appropriée.
namespace WorkerScope.Example;
public sealed class Worker(
ILogger<Worker> logger,
IServiceScopeFactory serviceScopeFactory)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
try
{
logger.LogInformation(
"Starting scoped work, provider hash: {hash}.",
scope.ServiceProvider.GetHashCode());
var store = scope.ServiceProvider.GetRequiredService<IObjectStore>();
var next = await store.GetNextAsync();
logger.LogInformation("{next}", next);
var processor = scope.ServiceProvider.GetRequiredService<IObjectProcessor>();
await processor.ProcessAsync(next);
logger.LogInformation("Processing {name}.", next.Name);
var relay = scope.ServiceProvider.GetRequiredService<IObjectRelay>();
await relay.RelayAsync(next);
logger.LogInformation("Processed results have been relayed.");
var marked = await store.MarkAsync(next);
logger.LogInformation("Marked as processed: {next}", marked);
}
finally
{
logger.LogInformation(
"Finished scoped work, provider hash: {hash}.{nl}",
scope.ServiceProvider.GetHashCode(), Environment.NewLine);
}
}
}
}
}
Dans le code précédent, pendant l’exécution de l’application, le service en arrière-plan :
- Dépend de IServiceScopeFactory.
- Crée un IServiceScope pour résoudre d’autres services.
- Résout les services délimités pour la consommation.
- Fonctionne sur le traitement des objets, puis leurs relais, puis les marque comme traités.
À partir de l’exemple de code source, vous pouvez voir comment les implémentations de IHostedService peuvent tirer parti des durées de vie de service étendues.
Services clés
Vous pouvez inscrire des services et effectuer des recherches basées sur une clé. En d’autres termes, il est possible d’inscrire plusieurs services avec différentes clés et d’utiliser cette clé pour la recherche.
Prenez par exemple le cas où vous avez différentes implémentations de l’interface IMessageWriter : MemoryMessageWriter et QueueMessageWriter.
Vous pouvez inscrire ces services à l’aide de la surcharge des méthodes d’inscription de service (évoquées précédemment) qui prennent en charge une clé en tant que paramètre :
services.AddKeyedSingleton<IMessageWriter, MemoryMessageWriter>("memory");
services.AddKeyedSingleton<IMessageWriter, QueueMessageWriter>("queue");
Ce key n’est pas limité à string. Il key peut s’agir de n’importe quoi object souhaité, tant que cela implémente Equals correctement.
Dans le constructeur de la classe qui utilise IMessageWriter, vous ajoutez le FromKeyedServicesAttribute pour spécifier la clé du service à résoudre :
public class ExampleService
{
public ExampleService(
[FromKeyedServices("queue")] IMessageWriter writer)
{
// Omitted for brevity...
}
}
KeyedService.AnyKey, propriété
La KeyedService.AnyKey propriété fournit une clé spéciale pour l’utilisation des services à clés. Vous pouvez enregistrer un service en utilisant KeyedService.AnyKey comme solution de repli qui correspond à n’importe quelle clé. Cela est utile lorsque vous souhaitez fournir une implémentation par défaut pour toute clé qui n’a pas d’inscription explicite.
var services = new ServiceCollection();
// Register a fallback cache for any key.
services.AddKeyedSingleton<ICache>(KeyedService.AnyKey, (sp, key) =>
{
// Create a cache instance based on the key.
return new DefaultCache(key?.ToString() ?? "unknown");
});
// Register a specific cache for the "premium" key.
services.AddKeyedSingleton<ICache>("premium", new PremiumCache());
var provider = services.BuildServiceProvider();
// Requesting with "premium" key returns PremiumCache.
var premiumCache = provider.GetKeyedService<ICache>("premium");
Console.WriteLine($"Premium key: {premiumCache}");
// Requesting with any other key uses the AnyKey fallback.
var basicCache = provider.GetKeyedService<ICache>("basic");
Console.WriteLine($"Basic key: {basicCache}");
var standardCache = provider.GetKeyedService<ICache>("standard");
Console.WriteLine($"Standard key: {standardCache}");
Dans l’exemple précédent :
- La demande
ICacheavec la clé"premium"retourne l’instancePremiumCache. - Faire une demande
ICacheavec toute autre clé (comme"basic"ou"standard") crée une nouvelleDefaultCacheavec l'option de secours deAnyKey.
Important
À compter de .NET 10, l’appel GetKeyedService() avec KeyedService.AnyKey lève un InvalidOperationException car AnyKey est destiné à être une alternative d’inscription plutôt qu'une clé de requête. Pour plus d’informations, consultez Résoudre les problèmes dans GetKeyedService() et GetKeyedServices() avec AnyKey.
Voir aussi
- Démarrage rapide : Principes de base de l’injection de dépendances
- Didacticiel : Utiliser l’injection de dépendances dans .NET
- Lignes directrices sur l'injection de dépendance
- Injection de dépendances dans ASP.NET Core
- Modèles de conférence NDC pour le développement d’applications DI
- Principes de dépendances explicites
- Inversion des conteneurs de contrôle et du modèle d’injection de dépendances (Martin Fowler)