Fermer

mars 21, 2022

Injection de dépendances en C# .NET


J'ai décidé d'écrire un tutoriel sur la façon d'accomplir l'injection de dépendances dans C # .NET, car la documentation Microsoft des bibliothèques d'injection de dépendances est malheureusement beaucoup trop clairsemée, et l'injection de dépendances MicrosoftDidacticiel est un peu alambiqué et compliqué.

Heureusement, l'implémentation de l'injection de dépendances par C# .NET est assez simple. À mon avis, c'est beaucoup plus simple que l'implémentation fournie par Spring Framework de Java. Si vous comprenez les bases du concept d'injection de dépendances mais que vous ne l'avez pas encore essayé en pratique, C# .NET pourrait être votre meilleur pari.

Voici un bref récapitulatif de ce qu'implique l'injection de dépendance. Si vous voulez plus de détails,Cet article J'ai écrit peut être utile.

En général, que l'injection de dépendances soit en jeu ou non, les classes peuvent spécifier des types appelésdépendances– qu'ils onta unrelations avec.

Dans l'injection de dépendances, les instances de classes ne sont pas responsables de la création d'instances de leurs dépendances. Au lieu de cela, un conteneur de gestion maintienta un relations avec les instances des classes, et l'utilisateur spécifie au conteneur quelles implémentations des dépendances il souhaite utiliser en appelant l'une des méthodes du conteneur, ou en écrivant ce qu'on appelle un "code de configuration" qui est interprété par le conteneur. Lors de l'exécution, le conteneur "injecte" ces implémentations dans les instances de classe.

Pourquoi utiliser l'injection de dépendance ? L'essentiel est deinterface séparée de l'implémentation . Pourquoi est-ce important? Je vous suggère de lire l'article lié ci-dessus pour plus de détails.

La première chose à savoir lors de l'apprentissage de l'injection de dépendances dans C# .NET est que Microsoft utilise une terminologie alternative lors de l'examen des concepts d'injection de dépendances. Si vous souhaitez comprendre la documentation Microsoft, vous devez connaître cette terminologie. Alors, voici un peu de vocabulaire :

Microsoft phraseSignification
servicedépendance
enregistrement des servicesle stockage des dépendances dans le conteneur de gestion
service de résolutionl'injection à l'exécution d'une dépendance

leServiceDescriptor la classe est ce qui représente un service (rappelons que « service » signifie « dépendance »). Le constructeur le plus terre-à-terre deServiceDescriptor est comme suit:

Publique ServiceDescriptor(Taper type de service,Taper type d'implémentation,ServiceLifetime durée de vie)

Ainsi, nous voyons qu'en C # .NET, un service englobe essentiellement le type de la dépendance, le type de l'implémentation préférée pour ladite dépendance et la "durée de vie" de la dépendance.

À mon avis, la "durée de vie" devrait vraiment s'appeler "multiplicité d'instanciation", puisque la valeur dedurée de vie dans le constructeur ci-dessus détermine si le conteneur de gestion doit ou non créer plusieurs instances de la dépendance et, si tel est le cas, comment procéder.

Spécifiquement,ServiceLifetime est unénumération qui peut prendre la valeurSingleton,TransitoireouPortée.

  • Singleton indique que le conteneur de gestion (que nous n'avons pas encore vu) garantira qu'une seule instance du service sera créée pendant toute la durée de vie du programme. Toutes les instances de classe qui dépendent du service partageront le même service.

  • Transitoire indique que le conteneur de gestion s'assurera qu'une nouvelle instance de la dépendance sera créée chaque fois qu'une instance de classe différente en aura besoin.

  • Le sens dePortée est un peu compliqué pour une première passe d'injection de dépendances en C# .NET. Si vous voulez en savoir plus, lisezce.

ServiceDescriptor Propriétés

Vous avez déjà vu leServiceDescriptor constructeur, qui est ce qui est le plus important en ce qui concerne la compréhensionServiceDescriptor . Pour un peu plus de détails, voici les propriétés publiques qui sont enveloppées par leServiceDescriptor:

Publique FonctionIServiceProvider,objet> ? ImplementationFactory{avoir ;  }
Publique objet? Instance de mise en œuvre{avoir ;  }
Publique Taper? Type de mise en œuvre{avoir ;  }
Publique ServiceLifetime Durée de vie{avoir ;  }
Publique Taper Type de service{avoir ;  }

Certains des éléments ci-dessus peuvent prêter à confusion, alors voici quelques notes de clarification :

  • Fonction représente une fonction qui prend un argument de typeT1 en entrée et renvoie un typeT2 instance en sortie. Ainsi, leImplementationFactory propriété est une fonction qui prend uneIServiceProvider en entrée et renvoie une instance de l'implémentation en sortie.ImplementationFactory peuvent être considérées comme des instructions d'emballage sur la façon de créer une instance de l'instance d'implémentation.

  • Pour tout typeJl'expressionT ? est un raccourci pourNullablece qui représente unenullableversion du typeJ . Un type est appelénullablesi des erreurs de compilation sontne pasjeté quand unnul la valeur dudit type est tentée d'être utilisée. Pour plus de contexte surNullable est, voir l'annexe ci-dessous.

Services d'enregistrement (ServiceDescriptors) avecIServiceCollection

Jusqu'à présent, nous savons comment représenter les services (dépendances) commeServiceDescriptor s. Nous allons maintenant apprendre à créer un conteneur de gestion et à enregistrer nos services auprès dudit conteneur.

Une instance de typeIServiceCollection est ce qui représentera notre conteneur de gestion. Microsoft nous fournit une implémentation de cette interface – leServiceCollection classe de laMicrosoft.Extensions.DependencyInjectionMicrosoft.Extensions.DependencyInjection namespace- nous n'avons donc pas à nous occuper de l'implémentation nous-mêmes.

Nous ne nous soucierons pas trop de la façon dont Microsoft implémenteIServiceCollectioncependant – ce qui compte vraiment, c'est ce queIServiceCollection spécifie comme interface.

De sa définition d'interface, nous pouvons voir queIServiceCollection spécifie l'extension à partir de trois interfaces qui s'étendent elles-mêmesCollectionet pas plus:

en utilisantMicrosoft.Extensions.DependencyInjection;
en utilisant Système.Collections.Générique;

interface publique IServiceCollection:ICollectionServiceDescriptor>,IEnumerableServiceDescriptor>,IListeServiceDescriptor> { }

Ainsi, on voit queIServiceCollection est essentiellement une interface avecCollection.

(Alors,IServiceCollection est interprété comme «Je{ServiceCollection}», qui signifie « interface à un ensemble de services », et non «{IService}Collection», ce qui signifierait « collection d'interfaces aux services » !).

Enregistrement de service via des méthodes d'extension pourIServiceCollection

Afin de stocker des services dans unIServiceCollectionnous devons activer l'accès à certainsméthodes d'extension pour IServiceCollection.

(Une méthode d'extension est une méthode d'instance d'une classe qui est ajoutée à la classeaprès la classe est définie. Confusément, vouspouvez ajouter des méthodes d'extension, qui sont des méthodes non abstraites, à une interface. Pour en savoir plus sur les méthodes d'extension, voir l'annexe ci-dessous).

Pour obtenir l'accès aux méthodes d'extension dont nous avons besoin, incluez simplement unà l'aide de Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions déclaration en haut du fichier.

Quelques méthodes d'extension importantes (avec le paramètre pour la classe étendue,IServiceCollectionomis) ajouté parServiceCollectionServiceExtensions sont:

AjouterSingleton(Taper type de service,Taper type d'implémentation);
AjouterSingleton(Taper type de service);

Étant des méthodes d'extension pourIServiceCollectionces méthodes sont invoquées sur une instance de typeIServiceCollection de la même manière que les méthodes d'instance habituelles. Par exemple, siprestations de service a le genreIServiceCollectionalors nous appellerions les méthodes ci-dessus en écrivant

prestations de service.AjouterSingleton(type de service,type d'implémentation);
prestations de service.AjouterSingleton(type de service);

Vous pouvez probablement supposer que ces deux méthodes ajoutent unServiceDescriptor de vieSingleton qui a le spécifiétype de service ettype d'implémentation à laIServiceCollection.

Il existe également des versions des méthodes ci-dessus pour lesquelles certaines combinaisons de paramètres sont maintenues fixes :

AjouterSingletonService,Mise en œuvre>();
AjouterSingletonService>();

AjouterSingletonService,Mise en œuvre>(FonctionIServiceProvider,Mise en œuvre> implémentationUsine)

Et, bien sûr, pour chaque méthode dont le nom estAjouterSingletonil y aura des méthodes correspondantes avec des noms deAjouter un transitoire etAjouter une portée qui effectuent la même tâche pour les services deTransitoire etPortée vies, respectivement.

Cependant, ces deux versions deAjouterSingletonqui spécifient l'instance qui doit être encapsulée par le service singleton, n'ont pasAjouter un transitoire ouAjouter une portée homologues, car cela n'aurait aucun sens de ne spécifier qu'une seule instance pourAjouter un transitoire ouAjouter une portée:

AjouterSingleton(Taper type de service,objet instance de mise en œuvre);
AjouterSingletonService>(Service exemple);

Résolution des services lors de l'exécution avecIServiceProvider

À ce stade, nous savons comment nous savons comment représenter les services (dépendances) commeServiceDescriptor s, comment créer comment créer un conteneur de gestion et comment enregistrer nos services avec le conteneur. Le dernier élément que nous devons aborder est celui de la configuration de la résolution des services au moment de l'exécution (c'est-à-dire la configuration de l'injection des dépendances au moment de l'exécution).

Supposons queprestations de service est unIServiceCollection (c'est-à-dire un conteneur de gestion) qui contient certainsServiceDescriptors (c'est-à-dire « services » ou dépendances).

Pour récupérer des services à partir d'un conteneur de gestion nomméprestations de service à l'exécution, nous allons d'abord obtenir une instance de typeIServiceProvider du conteneur de gestion*en stockant la valeur de retour deservices.BuildServiceProvider() . Ensuite, nous récupérons un service particulier en utilisant la seule méthode abstraite spécifiée dans leIServiceProvider interface:

Publique objet? ObtenirService(Taper type de service)

*services.BuildServiceProvider renvoie une instance deMicrosoft.Extensions.DependencyInjection.ServiceProviderMicrosoft.Extensions.DependencyInjection.ServiceProviderqui implémenteIServiceProvider.

Cette section est assez facultative.

Si vous en avez déjà unIServiceCollection instance et correspondantIServiceProvideret vous voulez en créer un autreIServiceCollection instance en utilisant les dépendances stockées dans le premierIServiceCollection exemple, vous pouvez utiliser ces méthodes d'extension pourIServiceCollection:

AjouterSingleton(Taper type de service,FonctionIServiceProvider,objet> usine);

AjouterSingletonService,Mise en œuvre>(FonctionIServiceProvider,Mise en œuvre> implémentationUsine);
AjouterSingletonService>(FonctionIServiceProvider,Service> implémentationUsine);

Cette annexe documente certaines fonctionnalités moins connues du langage C#.

? et les types de référence nullables

UNEnon nullabletype est un type pour lequel des erreurs de compilation sont générées lorsqu'une variable de ce type avecnul la valeur est tentée d'être utilisée. A l'opposé, unnullable type est un type pour lequel les erreurs du compilateur ne sont pas levées dans ladite situation. Vous pouvez toujours obtenir des erreurs d'exécution avec des types nullables, bien sûr ! L'intérêt des types non nullables est d'éviter les erreurs d'exécution en les interceptant à la compilation.

Selon leDocumentation Microsoft , tous les types de référence acceptaient la valeur Null avant C# 8.0. De nos jours (c'est-à-dire après C# 8.0), tous les types de référence sont non nullables par défaut.

Cependant, vous pouvez toujours utiliser des types nullables si vous le souhaitez vraiment. Pour tout typeJle typeNullable est nullable.?T est un raccourci pourNullable.

Méthodes d'extension

En C#, il est possible de définir des méthodes d'instance en dehors de la définition de classe correspondante. Les méthodes ainsi définies sont appeléesméthodes d'extension.

Les méthodes d'extension doivent être définies dans unstatique classe, et doit utiliser lace mot-clé de la manière suivante :

Publique classer Cls{ ... }

Publique statique classer Extension
{
   Publique statique entier extensionMethod1(ce Cls CL)
{entier uneValeur = 0;retourner uneValeur ;  }
   
   Publique statique entier extensionMethod2(ce Cls CL,entier argument)
{entier uneValeur = 0;retourner uneValeur ;  }
}

Les méthodes d'extension sont appelées de la même manière que les méthodes d'instance régulières : pour appeler les méthodes d'extension définies ci-dessus sur une instanceCL deClstu écriraiscls.extensionMethod() oucls.extensionMethod2(arg)respectivement.

Méthodes d'extension aux interfaces

De manière quelque peu déroutante, il est possible de définir des méthodes d'extension – exactement de la même manière que ci-dessus – pour les interfaces. Pour moi, cette possibilité va à l'encontre de l'intention de "l'interface" – les interfaces ne sont pas censées être associées à des implémentations réelles de méthodes. Mais toipouvez fais le. Il est également en fait impossible d'ajouter quelque chose comme une "méthode d'extension abstraite" à une interface. La bibliothèque standard C # utilise malheureusement beaucoup l'implémentation d'interfaces via des méthodes d'extension. Tant pis.

J'ai fait référence aux deux articles suivants pour développer ma compréhension deIServiceCollection:(1),(2).

A propos de l'auteur

Ross Grogan-Kaylor est consultant technique associé au bureau de Perficient à Minneapolis. Il aime s'engager avec des modèles structurels dans la syntaxe et dans les idées de haut niveau du développement logiciel.

Plus de cet auteur






Source link