IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Apprendre l'injection de dépendances avec ASP.NET Core
Un billet d'Hinault Romaric

Le , par Hinault Romaric

0PARTAGES

ASP.NET Core est bâti autour de la modularité. Les fonctionnalités du Framework sont regroupées à travers divers modules, qui peuvent être ajoutés assez facilement par le développeur en cas de besoin. Cette approche offre plus de souplesse au développeur qui livrera son application avec juste le nécessaire pour son fonctionnement.


La modularité et la flexibilité de ASP.NET Core s’appuient énormément sur l’injection de dépendances qui est désormais une caractéristique essentielle du Framework. En effet, ASP.NET implémente de façon native la gestion de l’injection de dépendances. Le développeur n’aura plus besoin d’outils tiers pour mettre en place cette bonne pratique orientée objet.

L’injection de dépendances peut à première vue sembler abstraite et pourrait même faire peur au développeur. Mais, son support natif par ASP.NET Core oblige tout développeur qui utilise le Framework à maitriser les principes de cette pratique et comment elle est mise en place dans ASP.NET Core.

Ne pas comprendre l’intérêt et l’implémentation de l’injection de dépendances peut ouvrir la voie à de nombreux travers et de mauvaises pratiques. Par exemple, le fait de disposer d’un conteneur d’inversion de contrôle (IoC) et l’utiliser ne signifie pas pour autant pratiquer de l’injection de dépendances. C’est quelque chose d’assez fréquent chez les personnes qui ne maitrisent pas de façon concrète cette bonne pratique.

Pourquoi l’injection des dépendances ?

Pour faire ressortir l’intérêt de l’injection de dépendances, je vais faire une analogie avec le code spaghetti très souvent utilisé en programmation. Le code mal écrit c’est comme un plat de spaghetti. « Il suffit de tirer sur un fil d'un côté de l'assiette pour que l'enchevêtrement des fils provoque des mouvements jusqu'au côté opposé. » Un code fortement couplé (avec des composants qui dépendent de nombreux autres composants) peut être sujet à de nombreux bogues, est difficilement maintenable et à comprendre. Une modification à un composant peut facilement impacter de nombreux autres composants.

Pourtant, en programmation orientée objet, des objets travaillent ensemble dans un modèle de collaboration ou il y a des contributeurs et des consommateurs. Il va de soi que ce modèle de programmation génère des dépendances entre les objets et les composants, devenant difficile à gérer lorsque la complexité augmente.


De plus, mettre en place des tests unitaires pour un code fortement couplé peut s’avérer très difficile.

Pour pallier cela, vous pouvez utiliser l’injection de dépendances. L’injection de dépendances prône le découplage entre les composants. En effet, un composant B doit dépendre d’une abstraction d’un composant A. Ainsi, le composant B n’a pas besoin de se préoccupé de comment le composant A est implémenté. De ce fait, l’implantation de A ou les changements qui pourront être apportés à ce dernier vont moins impacter le composant B.

De façon concrète, supposons que nous disposons d’une classe d’accès aux données (CategoriesRepository) pour la manipulation des catégories.

Sans injection des dépendances, l’exemple standard d’utilisation de cette classe devrait ressembler à ceci :

Code c# : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
public class CategoriesService 
    { 
  
        private CategoriesRepository _categoriesRepository = new CategoriesRepository(); 
  
      public List<Categorie> GetCategories() 
        { 
  
           return _categoriesRepository.GetAll(); 
        } 
  
    }


Ce code pose deux problèmes majeurs :

  1. La classe CategoriesService dépend fortement de la classe CategoriesRepository. Supposons un instant que la signature du constructeur de la classe CategoriesRepository change. La classe CategoriesService devra changer aussi, tout comme toute classe ou méthode qui instancie CategoriesRepository de la même façon ;
  2. La classe CategoriesService hérite de la responsabilité de la création de CategoriesRepository.


En appliquant l’injection de dépendances, le code ci-dessus devrait ressembler à ce qui suit :

Code c# : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CategoriesService  
    { 
  
        private CategoriesRepository _categoriesRepository; 
  
        public CategoriesService(CategoriesRepository categoriesRepository) 
        { 
            _categoriesRepository = categoriesRepository; 
        } 
  
        public List<Categorie> GetCategories() 
        { 
  
           return _categoriesRepository.GetAll(); 
        } 
  
    }

Dans cette nouvelle implémentation de CategoriesService nous avons déporté la responsabilité de l’initialisation de CategoriesRepository. De ce fait, les conséquences liées aux changements apportés à CategoriesRepository sur notre classe sont limitées.

Types d’injection de dépendances

Deux types d’injection de dépendances sont fréquemment utilisés : l’injection par propriété et l’injection par constructeur.

Injection de dépendances par propriété

La mise en place de l’injection de dépendances par propriété est assez simple. Il suffit d’offrir une propriété publique qui permettra de setter la dépendance que l’on veut injecter.

Un exemple concret est le suivant :

Code C# : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CategoriesService  
    { 
  
        private CategoriesRepository _categoriesRepository; 
  
         public CategoriesRepository CategoriesRepository { 
                  set{ 
                        _categoriesRepository = value; 
                     } 
             } 
  
        public List<Categorie> GetCategories() 
        { 
  
           return _ categoriesRepository.GetAll(); 
        }    
    }

Cette implémentation va permettre de déporter la responsabilité de l’initialisation de la dépendance au client qui va utiliser cette classe. Ce dernier le fera via la propriété publique qui est offerte.

Cette approche offre cependant quelques limitations. Le client sera par exemple à mesure d’appeler la méthode GetAll() sans initialiser la dépendance. Ce qui va produire une exception de type NullReferenceException. Il est possible toutefois de vérifier que la dépendance n’est pas nulle avant d’appeler cette dernière. Mais, cela ajoute du code supplémentaire.

Injection de dépendances par constructeur

L’injection de dépendances par constructeur est l’approche la plus couramment utilisée dans la mise en place de la pratique. Il s’agit simplement de passer la dépendance comme paramètre du constructeur. Son implémentation simple, que nous avons déjà présentée plus haut, est la suivante :

Code C# : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CategoriesService  
    { 
  
        private CategoriesRepository _categoriesRepository; 
  
        public CategoriesService(CategoriesRepository categoriesRepository) 
        { 
            _categoriesRepository = categoriesRepository; 
        } 
  
        public List<Categorie> GetCategories() 
        { 
  
           return _categoriesRepository.GetAll(); 
        } 
  
    }

L’injection par constructeur permet d’éviter l’utilisation d’une dépendance qui n’a pas été initialisée comme c’est le cas avec l’injection par propriété. À l’initialisation de cette classe, le développeur sera obligé de fournir une instance de la dépendance.

Conteneurs d’injection de dépendances

Suite à la mise en place de l’injection de dépendances, nos classes exploitent désormais des abstractions des dépendances utilisées. Ces abstractions sont représentées dans le code par des interfaces.

La classe appelante aura donc désormais la responsabilité d’instancier la dépendance en utilisant l’implémentation qui correspond à l’interface utilisée dans la classe appelée.

En procédant ainsi, l’instanciation d’un nombre important de dépendances des couches inférieures peut se retrouver au niveau de la couche supérieure. Ce qui va non seulement augmenter la quantité de code à écrire, mais également la maintenance. En effet, la couche supérieure devra identifier et instancier les dépendances correspondantes aux interfaces.

C’est à ce stade qu’interviennent les conteneurs d’inversion de contrôle (conteneur IoC – inversion of control).

Le conteneur d’IoC est en quelque sorte un catalogue des types abstrait (interfaces) et leur implémentation. Dans ce catalogue, chaque type abstrait est lié à son implémentation. Le conteneur d’IoC permettra d’obtenir pour chaque type abstrait appelé, l’implémentation correspondante.

C’est à vous de remplir ce catalogue via la configuration du conteneur IoC. Vous devez fournir pour chaque type abstrait l’implémentation correspondante et définir comment cette dernière sera instanciée (une seule fois pour l’ensemble de l’application, pour chaque objet qui l’utilise, etc.)

Dans la mise en place de l’injection de dépendances par constructeur, les dépendances sont passées en paramètre dans le constructeur de la classe. Lorsqu’une instance de cette dernière est chargée, ses dépendances sont automatiquement fournies par le conteneur d’IoC. Cela permet de consommer les dépendances sans avoir besoin d’instancier ces dernières.

Il existe sur le marché de nombreux conteneurs d’IoC, dont Unity, Ninject ou encore StructureMap. Mais, avec ASP.NET Core, vous n’avez pas besoin d’avoir recours à ceux-ci. Sauf, si vous avez besoin de fonctionnalités avancées.

En effet, ASP.NET Core embarque par défaut un service (représenté par l’interface IServiceProvider) permettant d’offrir les fonctionnalités minimales d’un conteneur d’IoC.

Dans ASP.NET Core, les dépendances sont perçues comme des services. C’est pourquoi le conteneur d’IoC est représenté par IServiceProvider (fournisseur de service). Dans la suite de cette section, le terme service fera référence aux dépendances gérées par l’IoC.

Configuration du conteneur d’IoC

La configuration du conteneur d’IoC consiste en l’ajout de vos services (interface et implémentation) dans le catalogue des types abstraits. Chaque fois que vous avez un type abstrait qui doit être résolu par le conteneur IoC, vous devez enregistrer ce dernier.

L’enregistrement des services se fait via la méthode ConfigureServices (IServiceCollection) de la classe Startup de votre projet.

Code c# : Sélectionner tout
1
2
3
4
// This method gets called by the runtime. Use this method to add services to the container. 
        public void ConfigureServices(IServiceCollection services) 
        { 
        }

L’ajout d’un nouveau service à la collection des services du conteneur IoC se fait en utilisant la méthode IServiceCollection comme suit :

Code c# : Sélectionner tout
services.Add(new ServiceDescriptor(typeof(ICategoriesRepository), typeof(CategoriesRepository), ServiceLifetime.Transient));

Le premier paramètre est le type abstrait (l’interface) du service, le deuxième est l’implémentation de ce dernier et le troisième comment il sera instancié.

Cycle de vie des Services

Le conteneur d’IoC a désormais la responsabilité de fournir les instances des services qui sont utilisés par l’application. Doit-il fournir la même instance d’un service à tous les objets qui l’utilisent ? Doit-il fournir à chaque objet une instance différente du service ?, etc.

ASP.NET Core offre trois options pour définir comment les services sont instanciés pour les objets appelant. Il s’agit de :

  • Transient;
  • Scoped;
  • Singleton.


Transient

Lorsqu’un service est enregistré dans le conteneur d’IoC avec Transient, cela signifie que pour chaque objet qui fera appel à ce service, le conteneur d’IoC va fournir une instance de ce dernier. Ce qui signifie qu’une instance du service ne sera jamais partagée à plus d’un objet par le conteneur d’IoC.

ASP.NET offre l’extension AddTransient pour l’utilisation de ce dernier. Pour enregistrer un service, vous devez procéder comme suit :

Code c# : Sélectionner tout
services.AddTransient<ICategoriesRepository, CategoriesRepository>();

Scoped

L’enregistrement d’un service avec Scoped signifie que celui-ci sera instancié à chaque requête. Concrètement, pour une requête HTTP vers l’application, tous les objets qui utilisent le service recevront la même instance de ce dernier du conteneur d’IoC.

ASP.NET offre l’extension AddScoped pour l’utilisation de ce dernier. Pour enregistrer un service, vous devez procéder comme suit :

Code c# : Sélectionner tout
services.AddScoped<ICategoriesRepository, CategoriesRepository>();

Singleton

Singleton est utilisé pour un service qui doit être instancié une seule fois et dont la même instance sera utilisée par tous les composants de l’application qui en auront besoin. Le service est créé pour le premier composant qui en fait la demande, et utilisé pour le reste.

Si votre application nécessite un Singleton, au lieu d’implémenter le pattern Singleton, il est recommandé d’utiliser ce cycle de vie offert par le conteneur d’IoC.

ASP.NET offre l’extension AddSingleton pour l’utilisation de ce dernier. Pour enregistrer un service, vous devez procéder comme suit :

Code c# : Sélectionner tout
services.AddSingleton<ICategoriesRepository, CategoriesRepository>();


Autres extensions offertes par le conteneur d’IoC

ASP.NET Core offre de nombreuses autres extensions pour l’enregistrement des autres fonctionnalités de la plateforme. Il s’agit notamment de AddMVC pour l’enregistrement du service pour le support de ASP.NET Core MVC, AddDbContext pour Entity Framework et AddIdenty pour l’authentification.

Code c# : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
// This method gets called by the runtime. Use this method to add services to the container. 
        public void ConfigureServices(IServiceCollection services) 
        { 
              // Add framework services. 
            services.AddEntityFrameworkSqlite() 
                .AddDbContext<BlogContext>(options => options.UseSqlite("Data Source=Blog.db")); 
  
             services.AddMvc(); 
  
             // Add application services. 
            services.Add<ICategoriesRepository, CategoriesRepository, Transient>(); 
        }

Injection de dépendances dans un contrôleur

Pour les contrôleurs MVC, l’injection de dépendances se fait via le constructeur. Dès lors que vous passez un service en paramètre au constructeur de votre contrôleur, ASP.NET Core s’attend à résoudre celui-ci en utilisant son conteneur d’IoC.

Vous devez donc toujours vous assurer que le service passé en paramètre est enregistré dans le conteneur d’IoC. Simon, vous obtiendrez l’exception suivante :

An unhandled exception occurred while processing the request.

InvalidOperationException: Unable to resolve service for type 'ControllerDI.Interfaces. ICategoriesRepository ' while attempting to activate 'ControllerDI.Controllers.HomeController'.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)

Ci-dessous un exemple de code du contrôleur avec l’injection de dépendances.

Code c# : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HomeController: Controller 
    { 
        private ICategoriesRepository _categoriesRepository; 
  
        public HomeController(ICategoriesRepository categoriesRepository) 
        { 
            _categoriesRepository = categoriesRepository; 
        } 
  
        public IActionResult Index() 
        { 
  
            return View(_categoriesRepository.GetAll()); 
        } 
    }


Injection de dépendances dans une action

Il peut arriver que le service dont vous avez besoin ne soit utilisé que par une seule action du contrôleur. Au lieu de passer celui-ci en paramètre au constructeur, vous pouvez directement passer ce dernier en paramètre à la méthode d’action qui l’utilise en ayant recours à l’attribut [FromServices] comme suit :

Code c# : Sélectionner tout
1
2
3
4
5
public IActionResult Index([FromServices] ICategoriesRepository categoriesRepository) 
        { 
  
            return View(categoriesRepository.GetAll()); 
        }

Injection de dépendances dans la vue

ASP.NET Core offre la prise en charge de l’injection de dépendances dans la vue, en utilisant le mot clé @inject. Cela peut être pratique pour l’affichage des données qui sont spécifiques à la vue, notamment la localisation.

Je recommande toutefois d’éviter d’avoir recours à cette option. Mal exploitée, elle peut rapidement devenir Anti-Pattern. Pour une vue qui a besoin de plus d’un modèle, au lieu d’utiliser l’injection dans la vue pour fournir les données provenant des autres modèles, vous devez plutôt utiliser le pattern ViewModel.

Contraintes liées à l’injection par constructeur avec ASP.NET Core

  • Le constructeur utilisé pour passer un paramètre par injection de dépendances doit être public. Sinon, le conteneur IoC sera incapable de résoudre la dépendance et vous obtiendrez l’exception InvalidOperationException;
  • La surcharge des constructeurs est prise en charge. Toutefois, une seule surcharge doit disposer des paramètres pouvant être fournis par l’injection de dépendances. Sinon, vous obtiendrez l’exception InvalidOperationException;
  • Les paramètres du constructeur qui ne sont pas fournis par le conteneur d’IoC doivent contenir une valeur par défaut. Sinon, vous obtiendrez l’exception InvalidOperationException.

Une erreur dans cette actualité ? Signalez-nous-la !