Juin 2016

Volume 31, numéro 6

Cet article a fait l'objet d'une traduction automatique.

Essential .NET - Guide d'injection des dépendances avec .NET Core

Par Mark Michaelis

Mark MichaelisDans mes deux derniers articles, « Journalisation avec .NET Core » (msdn.com/magazine/mt694089) et « Base de Configuration dans .NET » (msdn.com/magazine/mt632279), j’ai démontré comment la fonctionnalité principale de .NET peut être exploitée à partir d’un projet ASP.NET Core (project.json) et le projet de # .NET 4.6 C plus courant (*.csproj). En d’autres termes, en tirant parti de la nouvelle infrastructure n’est pas limité à ceux qui écrivent ASP.NET Core projets. Dans cet article, je vais continuer à plonger dans .NET Core, l’accent sur les fonctionnalités de .NET Core dépendance (DI) de l’injection de code et comment ils permettent une inversion du modèle de contrôle (IoC). Comme avant, exploitant les fonctionnalités .NET Core est possible à partir des fichiers CSPROJ « traditionnels » et les projets de type project.json émergentes. Pour l’exemple de code, cette fois-ci j’utiliserai XUnit à partir d’un projet project.json.

Pourquoi l’Injection de dépendance ?

Avec .NET, l’instanciation d’un objet est trivial par un appel au constructeur via l’opérateur new (autrement dit, MyService nouvelle ou quel que soit le type d’objet est que vous souhaitez instancier). Malheureusement, un appel à ceci impose une connexion étroitement couplée (une référence codée en dur) du code client (ou application) à l’objet instancié, ainsi qu’une référence à son package assembly/NuGet. Pour les types .NET ce n’est pas un problème. Toutefois, pour les types offre un « service », « telles que la journalisation, la configuration, au mode de paiement, notification ou même injection de dépendance, la dépendance peut être indésirable si vous souhaitez basculer l’implémentation du service que vous utilisez. Par exemple, dans un scénario, un client peut utiliser NLog pour la journalisation, tandis que dans un autre, ils peuvent choisir Log4Net ou Serilog. Et le client à l’aide de NLog préfèrent pas modifié leurs projet avec Serilog, une référence aux deux services de journalisation serait pas souhaitable.

Pour résoudre le problème de codage en dur une référence à l’implémentation du service, l’injection de dépendances fournit un niveau d’indirection telles qu’au lieu d’instancier le service directement avec l’opérateur new, le client (ou application) à la place demandera une collection de services ou les « factory » pour l’instance. En outre, au lieu de demander à la collection de service pour un type spécifique (créant ainsi une référence fortement couplée), vous demandez une interface (par exemple, ILoggerFactory) dans l’attente que le fournisseur de service (dans ce cas, NLog, Log4Net ou Serilog) implémente l’interface.

Le résultat est que lorsque le client fait référence directement l’assembly abstraite (Logging.Abstractions), qui définit l’interface de service, aucune référence à l’implémentation directe ne sera nécessaire.

Nous appelons le modèle de découpler l’instance réelle renvoyée au client d’Inversion de contrôle. Étant donné que plutôt que le client de déterminer ce qui est instancié, comme il le fait pour appeler explicitement le constructeur avec l’opérateur new, DI détermine ce qui est retourné. DI enregistre une association entre le type demandé par le client (généralement une interface) et le type qui sera renvoyé. En outre, l’injection de dépendances généralement détermine que la durée de vie de type retourné, en particulier, s’il y a une seule instance partagée entre toutes les demandes pour le type, une nouvelle instance de chaque demande, ou entre les deux.

Un besoin particulièrement courant d’injection de dépendance est dans les tests unitaires. Envisagez un service panier d’achat qui, à son tour, dépend d’un service de paiement. Imaginez que vous écrivez le service panier d’achat qui s’appuie sur le service de paiement et essayez d’unité de tester le service panier d’achat sans réellement appeler un service de paiement réel. Ce que vous souhaitez appeler est un service de paiement factice. Pour effectuer cette opération avec l’injection de dépendance, votre code demandait une instance de l’interface de service de paiement par le framework d’injection de dépendance plutôt que d’appeler, par exemple, nouvelle PaymentService. Ensuite, tout ce dont a besoin est pour le test unitaire pour l’infrastructure d’injection de dépendance pour retourner un service de paiement factice « configurer ».

En revanche, l’hôte de production peut configurer le panier d’achat pour utiliser une des options de service de paiement (plusieurs). Et peut-être plus important encore, les références serait uniquement à l’abstraction de paiement, plutôt qu’à chaque implémentation spécifique.

Fournir une instance de « service » au lieu du client de l’instancier directement est le principe fondamental de l’injection de dépendances. Ainsi, en fait, certaines infrastructures d’injection de dépendance dissocier de l’hôte de faisant référence à l’implémentation en prenant en charge un mécanisme de liaison qui est basé sur la configuration et la réflexion, au lieu d’une liaison de compilation. Ce découplage est connu comme le modèle de recherche de service.

.NET core Microsoft.Extensions.DependencyInjection

Pour tirer parti de l’infrastructure .NET Core DI, il vous suffit est une référence au package NuGet de Microsoft.Extnesions.DependencyInjection.Abstractions. Fournit l’accès à l’interface IServiceCollection, qui expose un System.IServiceProvider à partir duquel vous pouvez appeler la méthode GetService < TService >. Le paramètre de type, TService, identifie le type de service à récupérer (généralement une interface), donc le code d’application obtient une instance :

ILoggingFactory loggingFactor = serviceProvider.GetService<ILoggingFactory>();

Il existe des méthodes GetService non générique équivalentes qui ont un Type comme un paramètre (plutôt qu’un paramètre générique). Les méthodes génériques permettent d’affectation directement à une variable d’un type particulier, tandis que les versions non génériques nécessitent une conversion explicite, car le type de retour est l’objet. En outre, il existe des contraintes génériques lors de l’ajout du type de service afin qu’une conversion peut être évitée complètement lorsque vous utilisez le paramètre de type.

Si aucun type n’est inscrit avec le service lors de l’appel de méthode GetService, il retourne la valeur null. Cela est utile lorsqu’il est combiné avec l’opérateur de la propagation de null pour ajouter des comportements facultatif pour l’application. La méthode GetRequiredService similaire lève une exception lorsque le type de service n’est pas enregistré.

Comme vous pouvez le voir, le code est relativement simple. Toutefois, ce qui manque est comment obtenir une instance du fournisseur de services sur lequel appeler la méthode GetService. La solution est d’abord instancier simplement constructeur par défaut de ServiceCollection, puis inscrire le type que vous souhaitez que le service à fournir. Voici un exemple dans Figure 1, dans laquelle vous pouvez supposer que chaque classe (hôte, Application et PaymentService) est implémentée dans des assemblys séparés. En outre, alors que l’assembly hôte connaît les enregistreurs d’événements à utiliser, il n’existe aucune référence aux enregistreurs d’événements dans l’Application ou PaymentService. De même, l’assembly hôte n’a aucune référence à l’assembly PaymentServices. Les interfaces sont également implémentées dans des assemblys distincts « abstraction ». Par exemple, l’interface ILogger est défini dans l’assembly de Microsoft.Extensions.Logging.Abstractions.

Figure 1 l’enregistrement et qui demande un objet à partir de l’Injection de dépendance

public class Host
{
  public static void Main()
  {
    IServiceCollection serviceCollection = new ServiceCollection();
    ConfigureServices(serviceCollection);
    Application application = new Application(serviceCollection);
    // Run
    // ...
  }
  static private void ConfigureServices(IServiceCollection serviceCollection)
  {
    ILoggerFactory loggerFactory = new Logging.LoggerFactory();
    serviceCollection.AddInstance<ILoggerFactory>(loggerFactory);
  }
}
public class Application
{
  public IServiceProvider Services { get; set; }
  public ILogger Logger { get; set; }
    public Application(IServiceCollection serviceCollection)
  {
    ConfigureServices(serviceCollection);
    Services = serviceCollection.BuildServiceProvider();
    Logger = Services.GetRequiredService<ILoggerFactory>()
            .CreateLogger<Application>();
    Logger.LogInformation("Application created successfully.");
  }
  public void MakePayment(PaymentDetails paymentDetails)
  {
    Logger.LogInformation(
      $"Begin making a payment { paymentDetails }");
    IPaymentService paymentService =
      Services.GetRequiredService<IPaymentService>();
    // ...
  }
  private void ConfigureServices(IServiceCollection serviceCollection)
  {
    serviceCollection.AddSingleton<IPaymentService, PaymentService>();
  }
}
public class PaymentService: IPaymentService
{
  public ILogger Logger { get; }
  public PaymentService(ILoggerFactory loggerFactory)
  {
    Logger = loggerFactory?.CreateLogger<PaymentService>();
    if(Logger == null)
    {
      throw new ArgumentNullException(nameof(loggerFactory));
    }
    Logger.LogInformation("PaymentService created");
  }
}

Vous pouvez considérer le type ServiceCollection conceptuellement comme une paire nom-valeur, où le nom est le type d’un objet (en général une interface) que vous souhaitez ultérieurement récupérer et la valeur est le type qui implémente l’interface ou l’algorithme (délégué) pour la récupération de ce type. L’appel à AddInstance, dans la méthode Host.ConfigureServices Figure 1, par conséquent, inscrit que toutes les requêtes pour le retour de type ILoggerFactory la même instance LoggerFactory créée dans la méthode ConfigureServices. Par conséquent, Application et PaymentService sont en mesure de récupérer le ILoggerFactory sans aucune connaissance (ou même une référence d’assembly/NuGet) les enregistreurs d’événements sont implémentées et configurés. De même, l’application fournit une méthode de MakePayment sans connaître les service de paiement est utilisé.

Notez que ServiceCollection n’offrent des méthodes GetService ou GetRequiredService directement. Au lieu de cela, ces méthodes sont disponibles à partir de IServiceProvider retourné par la méthode ServiceCollection.BuildServiceProvider. En outre, les seuls services disponibles auprès du fournisseur sont ceux ajoutés avant l’appel à BuildServiceProvider.

Microsoft.Framework.DependencyInjection.Abstractions inclut également une classe d’assistance statique appelée ActivatorUtilities qui offre plusieurs méthodes utiles pour la gestion des paramètres du constructeur qui ne sont pas enregistrées avec IServiceProvider ObjectFactory délégué, ou dans les situations où vous souhaitez créer l’instance par défaut dans le cas où un appel à la méthode GetService retourne la valeur null (consultez bit.ly/1WIt4Ka#ActivatorUtilities).

Durée de vie de service

Dans Figure 1 j’invoque le IServiceCollection AddInstance < TService >(TService implementationInstance)-méthode d’extension. Instance est une des quatre options de durée de vie TService différents disponibles avec .NET Core DI. Il établit que non seulement l’appel à la méthode GetService retournera un objet de type TService, mais également que l’implementationInstance spécifique inscrit avec AddInstance est ce qui sera retourné. En d’autres termes, l’inscription auprès de AddInstance enregistre l’instance implementationInstance spécifique afin de pouvoir être renvoyé à chaque appel de la méthode GetService (ou GetRequiredService) avec le paramètre de type de la méthode AddInstance TService.

En revanche, la méthode d’extension IServiceCollection AddSingleton < TService > n’a aucun paramètre pour une instance et s’appuie sur le TService ayant un moyen d’instanciation via le constructeur. Si un constructeur par défaut fonctionne, Microsoft.Extensions.DependencyInjection prend également en charge les constructeurs par défaut dont les paramètres sont également enregistrés. Par exemple, vous pouvez appeler :

IPaymentService paymentService = Services.GetRequiredService<IPaymentService>()

et l’injection de dépendance se chargera de la récupération de l’instance concrète ILoggingFactory et son exploitation lors de l’instanciation de la classe PaymentService qui requiert un ILoggingFactory dans son constructeur.

Si aucune de ces moyens n’est disponible dans le type TService, vous pouvez utiliser à la place la surcharge de la méthode d’extension AddSingleton, qui prend un délégué du type Func < IServiceProvider, TService > implementationFactory, une méthode de fabrique pour l’instanciation TService. Si vous fournissez la méthode de fabrique ou non, la mise en oeuvre de la collection service garantit qu’il crée toujours une instance du type TService, donc vous assurer qu’il existe une instance singleton. Après le premier appel à la méthode GetService qui déclenche l’instanciation TService, la même instance est toujours renvoyée pour la durée de vie de la collection de service.

IServiceCollection inclut également les méthodes d’extension AddTransient (Type serviceType, Func < IServiceProvider, TService > implementationFactory) AddTransient (Type serviceType, qu’implementationType Type). Elles sont semblables à AddSingleton qu’elles retournent une nouvelle instance chaque fois qu’elles sont appelées, garantir que vous disposez toujours d’une nouvelle instance du type TService.

Enfin, il existe plusieurs AddScoped type méthodes d’extension. Ces méthodes sont conçues pour retourner la même instance d’un contexte donné et pour créer une nouvelle instance chaque fois que le contexte — appelé l’étendue — modifications. Le comportement du noyau d’ASP.NET est mappé sur le plan conceptuel à la durée de vie étendue. Pour l’essentiel, une nouvelle instance est créée pour chaque instance HttpContext et à chaque appel de méthode GetService au sein de l’objet HttpContext même, l’instance TService identique est renvoyée.

En résumé, il existe quatre options de durée de vie des objets retournés à partir de l’implémentation de collection de service : Instance, Singleton, temporaire et étendue. Les trois derniers sont définies dans l’énumération ServiceLifetime (bit.ly/1SFtcaG). Toutefois, instance, est manquante, car il s’agit d’un cas spécial d’inclus dans l’étendue dans laquelle le contexte ne change pas.

Précédemment j’appelée ServiceCollection comme point de vue conceptuel comme une paire nom-valeur avec le type TService servant à la recherche. L’implémentation réelle du type ServiceCollection s’effectue dans la classe de ServiceDescription (voir bit.ly/1SFoDgu). Cette classe fournit un conteneur pour les informations requises pour instancier le TService, à savoir le ServiceType (TService), qu’ImplementationType ou ImplementationFactory déléguer, ainsi que la ServiceLifetime. Outre les constructeurs ServiceDescriptor, il existe une multitude de méthodes de fabrique statiques sur ServiceDescriptor qui aident à instancier le ServiceDescriptor lui-même.

Quelle que soit la durée de vie qui vous inscrivez votre TService avec, le TService lui-même doit être un type référence, pas un type valeur. Chaque fois que vous utilisez un paramètre de type pour TService (plutôt qu’en passant de Type en tant que paramètre) le compilateur vérifiera cela avec une contrainte de classe générique. Une chose, toutefois, qui n’est pas vérifié utilise un TService de type objet. Vous souhaiterez être sûr éviter cela, ainsi que d’autres interfaces non uniques (tels que IComparable, par exemple). La raison est que si vous vous inscrivez quelque chose de type object, quel que soit le TService que vous spécifiez dans l’appel de la méthode GetService, l’objet enregistré comme un type TService sera toujours retourné.

Injection de dépendance pour l’implémentation de l’injection de dépendance

ASP.NET tire parti de l’injection de dépendances pour autant que, en fait, vous pouvez l’injection de dépendances au sein de l’infrastructure d’injection de dépendance elle-même. En d’autres termes, vous n’êtes pas limité à l’utilisation de la mise en oeuvre ServiceCollection du mécanisme d’injection de dépendance dans Microsoft.Extensions.DependencyInjection. Au lieu de cela, aussi longtemps que vous avez des classes qui implémentent IServiceCollection (défini dans Microsoft.Extensions.DependencyInjection.Abstractions ; consultez bit.ly/1SKdm1z) ou IServiceProvider (défini dans l’espace de noms System du framework de lib .NET Core) vous pouvez remplacer votre propre infrastructure d’injection de dépendance ou exploiter parmi les autres infrastructures DI établis, notamment Ninject (ninject.org, avec crient out à @IanfDavis pour son travail cette maintenance au fil des années) et Autofac (autofac.org).

Un mot sur ActivatorUtilities

Microsoft.Framework.DependencyInjection.Abstractions inclut également une classe d’assistance statique qui offre plusieurs méthodes utiles dans le traitement des paramètres du constructeur qui ne sont pas enregistrées avec IServiceProvider, un délégué ObjectFactory personnalisé ou les situations où vous souhaitez créer une instance par défaut dans le cas où un appel à la méthode GetService retourne la valeur null. Vous pouvez trouver des exemples où cette classe utilitaire est utilisée dans l’infrastructure MVC et la bibliothèque SignalR. Dans le premier cas, une méthode avec une signature de CreateInstance < T > (fournisseur IServiceProvider, [] paramètres de l’objet params) existe qui permet de transmettre des paramètres de constructeur à un type inscrit avec l’infrastructure d’injection de dépendance pour les arguments qui ne sont pas enregistrées. Vous pouvez également disposer d’une performance requise que les fonctions lambda nécessitent pour générer vos types être compilé les expressions lambda. La méthode CreateFactory (instanceType de Type, Type [] argumentTypes) qui retourne un ObjectFactory peut être utile dans ce cas. Le premier argument est le type recherchée par un consommateur, et le deuxième argument est tous les constructeur types, dans l’ordre, qui correspondent au constructeur du premier type que vous souhaitez utiliser. Dans son implémentation, ces éléments sont condensées jusqu'à une expression lambda compilée qui sera extrêmement performant lorsqu’elle est appelée plusieurs fois. Enfin, le GetServiceOrCreateInstance < T >(IServiceProvider provider) méthode fournit un moyen simple de fournir une instance par défaut d’un type qui peut éventuellement enregistrée dans un emplacement différent. Ceci est particulièrement utile dans le cas où vous autorisez DI avant l’appel, mais si qui ne se produit pas, vous avez une implémentation de secours.

Synthèse

Comme avec la journalisation de base .NET et la Configuration, le mécanisme de .NET Core DI fournit une implémentation relativement simple de ses fonctionnalités. Pendant que vous êtes peu probable que l’injection de dépendances des fonctionnalités plus avancées de certains des autres infrastructures, la version de .NET Core est léger et un excellent moyen pour commencer. En outre (et encore une fois, comme la journalisation et la Configuration), l’implémentation .NET Core peut être remplacée par une implémentation plus mature. Par conséquent, vous pouvez envisager tirant parti de l’infrastructure .NET Core DI comme un « wrapper » par le biais duquel vous pouvez brancher dans d’autres infrastructures d’injection de dépendance en cas de besoin à l’avenir. De cette façon, vous n’êtes pas obligé de définir votre propre wrapper DI « custom », mais pourrez tirer parti du .NET Core comme un standard pour lesquels une application cliente/peut incorporer une implémentation personnalisée.

Une chose à noter concernant ASP.NET Core est qu’elle tire parti de l’injection de dépendances dans l’ensemble. Ceci est sans aucun doute une très pratique si vous en avez besoin et il est particulièrement important lorsque vous essayez de remplacer des implémentations factices d’une bibliothèque dans vos tests unitaires. L’inconvénient est que plutôt qu’un simple appel à un constructeur avec l’opérateur new, la complexité de l’inscription de l’injection de dépendances et les appels de méthode GetService est requis. Je ne peux pas vous aider à mais se demandent si peut-être le langage c# peut simplifier cette, mais, en fonction de la conception de c# 7.0 actuelle, qui n’est pas produire de si tôt.


Mark Michaelisest le fondateur de IntelliTect, où il sert de son poste d'architecte en chef technique et un formateur. Depuis près de deux décennies, il a été MVP Microsoft et un directeur régional Microsoft depuis 2007. Michaelis fait plusieurs logiciels conception révision équipes Microsoft, notamment c#, Microsoft Azure, SharePoint et Visual Studio ALM. Il participe à des conférences de développeurs et a écrit de nombreux ouvrages, y compris sa plus récente, « Essential c# 6.0 (5e édition) » (itl.tc/EssentialCSharp). Contactez-le sur Facebook à facebook.com/Mark.Michaelis, sur son blog à l'adresse IntelliTect.com/Mark, sur Twitter : @markmichaelis ou par courrier électronique à mark@IntelliTect.com.

Je remercie les experts techniques IntelliTect suivants d’avoir relu cet article : Kelly Adams, Kevin Bost, Ian Davis et Phil Spokas