Comment marche-t-elle donc cette injection de dépendances intégrée à ASP.NET Core ?

Introduction

L’injection de dépendances (DI en anglais) est un « pattern » qui peut aider les développeurs à découpler les différentes parties de leurs applications. La DI fournit un mécanisme pour la construction de graphes de dépendances qui sont indépendantes de la définition des classes.

.NET Core supporte le « design pattern » de DI, lequel est une technique pour réussir l’inversion de Dépendances (le « D » sur les principes « SOLID ») entre les classes et leurs dépendances.

Avant .NET Core, la seule façon d’avoir de la DI dans une application était d’utiliser des frameworks tiers comme par exemple : Autofac, Ninject, StructureMap et bien plus d’autres encore.

Quoiqu’il en soit la DI a un traitement préférentiel dans ASP.NET Core.

Le problème

Le principe d’inversion des dépendances établit que :

  • Les modules de niveaux supérieurs ne devraient pas dépendre des modules de niveaux inférieurs. Ces deux types de modules devraient dépendre d’abstractions
  • Les abstractions ne devraient pas dépendre de détails. Les détails devraient dépendre des abstractions

Pour mieux comprendre les phrases précédentes, en tant que développeur, il est important de se demander :

  • Si je n’ai jamais dû modifier beaucoup de code à cause d’un nouveau besoin tout simple ?
  • Si je n’ai jamais eu du mal à refactoriser une partie de mon code ?
  • Si j’ai eu du mal à écrire mes tests unitaires parce qu’il y avait des composants qui dépendaient d’autres composants ?

Si la réponse à n’importe laquelle de ces questions est « Oui », peut-être que le code contient des dépendances. Quand un composant dépend trop fortement d’un autre la maintenance devient très compliquée et cela peut, bien sûr, engendrer des problèmes assez coûteux.

Regardez le code suivant :

class Voiture : IVehicule {
// …
}

class Transport

{

private readonly IVehicule _vehicule;

public Transport(IVehicule vehicule)
{

_vehicule = vehicule;

}

}

Dans cet exemple, Transport dépend de IVehicule et quelque part dans le code vous devriez construire une nouvelle instance de Transport et spécifier qu’elle dépend de de IVehicule comme il suit :

var voiture = new Voiture();

var transport = new Transport(voiture);

Il y a quelques problèmes avec ça. En premier, ce code ne respecte pas le principe d’Inversion des Dépendances parce que la classe consommatrice dépend explicitement des types concrets Transport et Vehicule. Deuxièmement cela brise le graphe de dépendances et peut rendre les tests unitaires très difficiles (car Transport et Vehicule ne peuvent pas être « mockés »).

Le « Composition Root pattern » dicte que tout le graphe de dépendances devrait être composé dans un seul endroit « si proche que possible du point d’entrée de l’application » (la classe Startup sur ASP.NET Core et la classe Program sur .NET Core).

La solution apportée par ASP.Net Core

La DI intégrée sur .NET Core fournit le mécanisme d’IoC via un concept souvent appelé « Conteneur ». Ce « conteneur » s’occupe de gérer le chargement des instances, l’injection et la durée de vie (lifetime) des dépendances de l’application. Cela consiste à inverser le contrôle de l’instanciation des composants depuis les consommateurs vers le « container », d’où le terme « Inversion de Contrôle ».

Pour faire ceci vous devez simplement enregistrer les services dans le « container f», puis vous pouvez charger les services de niveaux supérieurs. Le framework injectera tous les services « enfants » à votre place.

La solution sera présentée avec des exemples de code.

Concepts clef

  • Le principe d’Inversion de Dépendances : Ceci est un principe de conception logicielle. Il suggère une solution au problème des dépendances mais le principe ne définit pas comment les implémenter ou quelle technique utiliser
  • Inversion de Contrôle (IoC) : C’est une façon d’appliquer le principe d’Inversion des Dépendances. L’IoC est le mécanisme qui permet aux composants des niveaux supérieurs de dépendre d’abstractions plutôt que de l’implémentation concrète des composants des niveaux inférieurs.
  • Injection de dépendances : Ceci est un Design Pattern pour implémenter l’IoC. Elle permet d’injecter l’implémentation concrète d’un composant de niveau inférieur dans un composant de niveau supérieur
  • Conteneur IoC : Aussi connu en tant que « Conteneur DI ». C’est un framework qui implémente une DI automatique pour les composants

Approches pour l’Injection de Dépendances

  • Injection par Constructeur : Une des techniques DI les plus populaires. Avec cette approche vous créez une instance de votre dépendance et vous la passez en tant qu’argument au constructeur de la classe dépendante
  • Injection par Méthode : Dans ce cas vous créez une instance de la dépendance et vous la passez à une méthode spécifique de la classe dépendante
  • Injection par Propriété : Cette approche vous permet d’assigner l’instance de votre dépendance à une propriété spécifique de la classe dépendante

Durée de vie des Services

Parfois vous avez besoin une instance individuelle de votre dépendance qui « vivra » pendant toute la durée de vie de votre application. Ceci peut être utile pour un service de type « logger » ou « helper », mais c’est inacceptable pour d’autres services. Le « conteneur » IoC vous permet de contrôler la durée de vie (lifetime) d’un service enregistré. Quand vous enregistrez un service en spécifiant une durée de vie, le « conteneur » l’effacera automatiquement comme il faut. Il y a trois types de durée de vies pour les services :

  • Singleton : Ce type de durée de vie crée une instance du service disponible tant que l’application restera en exécution. Vous devez enregistrer un service de type « Singleton » en utilisant la méthode Add() / AddSingleton()
  • Transient : En utilisant ce type de durée de vie, votre service sera créé à chaque fois qu’il sera demandé. Cela veut dire, par exemple, qu’un service injecté dans le constructeur d’une classe durera “autant de temps” que l’instance de cette classe. Pour créer un service avec une durée de vie « Transient » vous devez utiliser la méthode AddTransient()
  • Scoped : ce type de durée de vie « Scoped » vous permet de créer une nouvelle instance d’un service par chaque demande d’un client. Ceci est particulièrement utile dans le contexte d’ASP.NET car cela vous permet de partager la même instance de service pour la durée de traitement de la demande http. Pour activer la durée de vie « Scoped » vous devez utiliser la méthode AddScoped()

Au boulot !

Bien ceci est surtout assez abstrait. Bon, cet article vous donnera des exemples de code pour clarifier les concepts sur les dépendances et les techniques pour la mitiger. Même si les concepts généraux sont valides pour n’importe quel framework et langage de programmation cet article se base principalement sur le framework .NET Core.

Pour maintenir cet article « relativement » court on va plutôt se concentrer sur une des techniques de DI les plus populaires : « Injection par Constructeur ». On va commencer par l’implémentation de la DI sur une application ASP.NET Core MVC, suffisamment simple pour illustrer la durée de vie des services.

Vous pouvez soit suivre les étapes suivantes ou tout simplement télécharger le projet ici.

Créez une nouvelle application ASP.NET Core Web

Project type

Saisissez le nom de la solution et le chemin physique

Choisissez un projet de type Web Application MVC

Project type 3

Etat initial du projet

Regardez le Startup

Ajoutez un dossier « Services » à la racine du projet

Services

Créez des Services et des Interfaces

Créez les fichiers suivants dans le dossier « Services » : Le « IOperation » s’utilise juste comme un contrat qui stipule que qui que ce soit qui l’implémente doit forcément avoir une propriété « Id » de type Guid avec un « getter ». Les classes « xxxxOperation » font toutes la même chose, quand elles sont instanciées : elles assignent la propriété « Id ».

  • IOperation:

public interface IOperation

{

Guid Id { get; }

}

  • SingletonOperation:

public interface ISingletonOperation : IOperation
{
}

public class SingletonOperation : ISingletonOperation
{

public Guid Id { get; }public SingletonOperation()
{

Id = Guid.NewGuid();

}

}

  • TransientOperation:

public interface ITransientOperation : IOperation
{
}

public class TransientOperation : ITransientOperation
{

public Guid Id { get; }public TransientOperation()
{

Id = Guid.NewGuid();

}

}

  • ScopedOperation:

public interface IScopedOperation : IOperation
{
}
public class ScopedOperation : IScopedOperation
{

public Guid Id { get; }    public ScopedOperation()
{

Id = Guid.NewGuid();

}

}

  • MyService : Ce service sera injecté dans le HomeController. Il nous aidera à obtenir une nouvelle instance de chaque service avec un seul appel depuis le constructeur du HomeController.

public interface IMyService
{
}

public class MyService : IMyService
{

private readonly ISingletonOperation _singletonOperation;
private readonly ITransientOperation _transientOperation;
private readonly IScopedOperation _scopedOperation;public MyService
(

ISingletonOperation singletonOperation,
ITransientOperation transientOperation,
IScopedOperation scopedOperation

)
{

_singletonOperation = singletonOperation;
_transientOperation = transientOperation;
_scopedOperation = scopedOperation;

}

}

Jetez un premier coup d’œil au HomeController

Donc maintenant vous pouvez continuer à modifier le HomeController en ajoutant trois nouvelles propriétés et en injectant les dépendances avec injection par constructeur comme il suit :

private readonly ISingletonOperation _singletonOperation;
private readonly IScopedOperation _scopedOperation;
private readonly ITransientOperation _transientOperation;
private readonly IMyService _myService;
private readonly ILogger<HomeController> _logger; public HomeController
(

ILogger<HomeController> logger,
ISingletonOperation singletonOperation,
ITransientOperation transientOperation,
IScopedOperation scopedOperation,
IMyService myService

)
{

_singletonOperation = singletonOperation;
_transientOperation = transientOperation;
_scopedOperation = scopedOperation;
_myService = myService;
_logger = logger;

}

Vous n’avez pas à vous préoccuper des méthodes de HomeController, vous n’allez pas les utiliser.

Vous pouvez désormais lancer l’application en appuyant F5 ou le bouton exécuter sur Visual Studio.

Vous avez alors le message d’erreur suivant :

“InvalidOperationException: Unable to resolve service for type ‘DI.LifeTimes.WebApp.Services.ISingletonOperation’ while attempting to activate ‘DI.LifeTimes.WebApp.Controllers.HomeController’.”

Cette erreur signifie que l’application a essayé de résoudre le paramètre ISingletonOperation dans le constructeur du HomeController mais elle n’a pas réussi.

Touches finales

Cet exemple de code a initialisé le travail avec l’utilisation d’abstractions au lieu de classes concrètes et l’injection par constructeur, mais vous n’avez pas encore configuré votre conteneur DI /  IC !

C’est bien la raison pour laquelle l’application ne marche pas… encore.

Pour régler ceci vous devez ajouter les lignes suivantes dans la méthode ConfigureServices dans Startup :

services.AddSingleton<ISingletonOperation, SingletonOperation>();
services.AddTransient<ITransientOperation, TransientOperation>();
services.AddScoped<IScopedOperation, ScopedOperation>();
services.AddTransient<IMyService, MyService>();

Vous devriez alors avoir quelque chose comme ça :

Avant que vous ne relanciez l’application, mettez un point d’arrêt à la fin du constructeur du HomeController :

Ce point d’arrêt vous permet de vérifier la valeur de l’objet dans le constructeur.

Relancez maintenant l’application !

Pour résumer…

Quel est l’intérêt de tout ce que vous avez fait ?

Avant que l’on ne vérifie chaque objet dans le constructeur, vous devriez vous rappeler de la définition de chacune des durées de vie de la DI et essayer de l’appliquer pour cet exemple.

  • _singletonOperation : une fois que vous avez lancé l’application sa valeur sera toujours la même

Le Guid généré pour _singletonOperation est « 2efb6278-ca01–46ab-ab8f-1a110599adea »
  • _transientOperation : sa valeur changera à chaque fois que le service TransientOperation sera appelé

Le Guid généré pour _transientOperation est “736dc453–8071–4e09-bd0b-730a9da48691”
  • _scopedOperation : sa valeur changera pour chaque nouvelle demande au serveur (donc ça se traduit pour vous par rafraîchir la « home page » de votre navigateur

La Guid généré pour _scopedOperation est « 4b17c40d-6643–4056–9412-e307e1a8219f »

Comme vous avez précédemment lu dans cet article, _myService est juste utilisé pour obtenir rapidement des nouvelles instances des trois différents services de l’application.

  • _myService : Logiquement si vous avez tout fait correctement, _myService devrait avoir les mêmes valeurs que _singletonOperation et _scopedOperation mais il devrait avoir des valeurs différentes pour _transientOperation

_myService._singletonOperation est égal à _singletonOperation

_myService._scopedOperation est égal à _scopedOperation

_myService._transientOperation est différent à _transientOperation

Félicitation ! Vous avez réussi !

Ceci prouve que tout fonctionne comme prévu.

Pour finalement vérifier les autres durées de vie, vous devriez appuyer sur F5 sur Visual Studio pour continuer l’exécution de la demande HTTP actuelle, puis rafraîchir la home page du site web. Ceci lancera une nouvelle demande vers le serveur.

Dans cette deuxième demande HTTP, _singletonOperation et _myService.singletonOperation devraient être égaux.

_scopedOperation et _myService._scopedOperation devraient être égaux mais la valeur devrait être différente à celle de la première demande HTTP.

_transientOperation et _myService._transientOpertation devraient être différent et ces valeurs devraient être encore différentes à celle de la première demande HTTP.