Cet article fait partie d’une série consacrée à l’injection de dépendance dans MVC 3 :

Dans cet article nous allons nous intéresser plus particulièrement à l’enregistrement de notre containeur DI auprès du Framework MVC 3. Mais avant de rentrer dans le vif du sujet, nous allons faire un petit rappel sur le fonctionnement actuel tel qu’il est connu depuis la version MVC 1.

Le code source pour cet article est présent sur mon Repo ici vs2010solution_43E39653

IControllerFactory

La création des contrôleurs dans le Framework ASP.NET MVC est l’endroit où on peut prendre la main pour introduire l’injection de dépendance. Ce point d’extension est présent depuis le Framework MVC 1 est peut être toujours utilisé dans la version MVC 3. Il s’agit de l’interface IControllerFactory qui est responsable de localiser et de créer des contrôleurs. Elle a était clairement introduite pour permettre l’injection de dépendance des contrôleurs.

IControllerFactory est un point d’enregistrement unique. Il existe également le point d’enregistrement statique pour ce service qui est ControllerBuilder.Current.SetControllerFactory.

En interne dans le Framework MVC, l’implémentation par défaut de IControllerFactory est la DefaultControllerFactory. Son implémentation est basée sur Activator.CreateInstance qui crée le contrôleur suivant la convention bien connue (récupération du nom de contrôleur de la requête http, omission du suffixe “Controller” et génération du type correspondant pour la méthode Activator.CreateInstance(Type controllerType). Donc dans le fonctionnement par défaut, un contrôleur ne pouvait avoir que le constructeur sans paramètres.

Pour modifier ce comportement, et par conséquent permettre l’injection de dépendance dans les contrôleurs nous devions :

  1. Créer notre propre controller factory soit par l’implémentation de l’interface IControllerFactory ou par la dérivation de la classe DefaultControllerFactory. Bien souvent la dérivation était suffisante pour garder le comportement de base comme fallback. Notre contrôleur factory prend en paramètre du constructeur l’instance de containeur DI (unity, structuremap, ninject, autofac, etc…) qui sert à la résolution du contrôleur et le passage des dépendances.
  2. Dans le cas de l’interface il fallait implémenter toutes les méthodes. Dans le cas de la dérivation il fallait au moins surcharger la méthode IController CreateController(RequestContext requestContext, string controllerName). La bonne pratique voudrait également qu’on surcharge la méthode void ReleaseController(IController controller).
  3. Un fois notre implémentation terminée nous indiquons au Framewrok MVC que pour la localisation et la création des instances des contrôleurs nous voulons utiliser notre propre controller factory qu’on vient de créer. Ceci est fait par le point d’enregistrement statique ControllerBuilder.Current.SetControllerFactory généralement appelée dans le Global.asax.

Je ne vais pas rappeler le code qui permet de créer sa propre controller factory supportant l’injection de dépendance car il y a de nombreux posts sur internet qui le démontrent. Le but ici est le MVC 3 et son fonctionnement.

DefaultControllerFactory

Comme mentionné plus haut dans l’article, si vous n’implémentez pas l’interface IControllerFactory  ou ne dérivez pas la classe DefaultControllerFactory alors le Framework MVC 3 utilisera par défaut la classe DefaultControllerFactory. Des modifications ont été apportées à cette classe pour introduire l’injection de dépendance. La première modification été l’introduction de l’interface IControllerActivator. Nous allons en parler dans l’article suivant, mais pour le moment tout ce qu’il faut savoir ce que cette nouvelle interface est introduite pour la création des contrôleur. Ensuite, l’implémentation par défaut de cette interface est la classe DefaultControllerActivator. Si vous ne fournissez pas votre implémentation de IControllerActivator alors la classe DefaultControllerActivator sera utilisée. La particularité de cette classe, est qu’elle prend en paramètre l’implémentation de IDependencyResolver pour résoudre et créer les contrôleurs. Un petit schéma permettra de mieux comprendre ce qui se passe:

DefaultControllerFactory

A noter que la classe DependencyResolver n’implémente pas IDependencyResolver. C’est un point d’enregistrement statique qui utilise en interne statiquement soit votre implémentation de IDependencyResolver, soit une implémentation par défaut sous forme de DefaultDependencyResolver (création avec Activator.CreateInstance), soit DelegateBaseDependencyResolver (résolution par de délégués, factories).

Enregistrement de votre containeur DI dans le Framework MVC 3

Toute cette théorie c’est beau ! Mais passons un peu à la pratique pour comprendre comment cela se passe dans la vraie vie. Pour cela j’ai préparé une toute petite application dont l’architecture n’a rien de transcendant :

SchémaAppliMvc

Note pour les débutants: C’est ne architecture “Domain centric” donc centrée sur le domaine. C’est ce qu’on utilise le plus souvent lorsqu’on utilise l’injection de dépendance.

Ce qu’on veut c’est de consommer dans le contrôleur MVC “HomeController” une instance implémentant IOrderService fourni au run-time.

Voyons le contrôleur par défaut :

   1: public class HomeController : Controller
   2:     {
   3:         public ActionResult Index()
   4:         {
   5:             ViewBag.Message = "Welcome to ASP.NET MVC!";
   6:  
   7:             return View();
   8:         }
   9:  
  10:         // d'autres actions
  11:     }

Nous allons le modifier pour lui injecter le service IOrderService afin qu’il puisse l’utiliser pour effectuer différentes opérations. La nouvelle version ressemble à ceci :

   1: public class HomeController : Controller
   2: {
   3:     private IOrderService _orderService;
   4:  
   5:     // constructeur pour permettre la reception de notre service.
   6:     public HomeController(IOrderService orderService)
   7:     {
   8:         if (orderService == null)
   9:             throw new ArgumentNullException("orderService", "Le service de commande ne peut pas être null");
  10:  
  11:         _orderService = orderService;
  12:     }
  13:     // actions....
  14: }

Si on essaie d’exécuter notre application nous obtenons une belle erreur :

ScreenErrorParametlessCtr

Tout ceci est normal, car tant que ne n’avons pas fourni notre implémentation de IDependencyResolver, en interne, la DefaultControllerFactory utilisera le DefaultDependencyResolver qui fait appel tout simplement à Activator.CreateInstance ce qui nécessite d’avoir un constructeur sans paramètres.

Il y a trois différentes manières d’enregistrer votre containeur DI.

  • En fournissant une implémentation de IDependencyResolver.
  • En fournissant une implémentation de IServiceLocator de CSL (il n’y a pas de dépendance dans MVC sur CSL donc la réflexion est utilisée pour vérifier que c’est une implémentation de IServiceLocator ).
  • Ad-hoc resolver basé sur des fonctions avec des signatures qui correspondent.

Nous allons voir chacune des ces méthodes.

1. Implémentation de IDependencyResolver avec Unity.

La première manière d’enregistrer notre containeur DI auprès du Framework MVC 3 et l’implémentation de IDependencyResolver. J’ai choisi Unity sans une raison particulière. Je trouve que c’est un bon containeur DI et largement utilisé dans des entreprises. Nous allons donc créer une classe UnityDependencyResolver pour créer les contrôleurs et leur fournir les dépendances nécessaires:

   1: public class UnityDependencyResolver : IDependencyResolver
   2: {
   3:     private readonly IUnityContainer _container;
   4:  
   5:     public UnityDependencyResolver(IUnityContainer container)
   6:     {
   7:         if (container == null)
   8:             throw new ArgumentNullException("container", "The container cannot be null");
   9:  
  10:         _container = container;
  11:     }
  12:  
  13:     public object GetService(Type serviceType)
  14:     {
  15:         object instance;
  16:  
  17:         try
  18:         {
  19:             instance = _container.Resolve(serviceType);
  20:         }
  21:         catch
  22:         {
  23:             instance = null;
  24:         }
  25:  
  26:         return instance;
  27:     }
  28:  
  29:     public IEnumerable<object> GetServices(Type serviceType)
  30:     {
  31:         IEnumerable<object> instances;
  32:  
  33:         try
  34:         {
  35:             instances = _container.ResolveAll(serviceType);
  36:         }
  37:         catch
  38:         {
  39:             instances = new object[] { };
  40:         }
  41:  
  42:         return instances;
  43:     }
  44: }

Comme vous le voyez ce n’est pas compliqué. En paramètre du constructeur de notre classe UnityDependencyResolver nous lui passons une instance de notre containeur Unity. Ce qui est important de noter ce que la méthode object GetService(Type serviceType) doit retourner null si le service ne peut pas être résolu par Unity, et la méthode IEnumerable<object> GetServices(Type serviceType) doit retourner une collection vide. Si ce n’est pas respecté vous allez avoir une exception au run-time.

Il ne nous reste que d’enregistrer notre IDependencyResolver dans le fichier Global.asax avec la méthode DependencyResolver.SetResolver():

   1: IUnityContainer container = new UnityContainer();
   2:  
   3: // enregistrer les types dans unity (ex: IOrderService, OrderService)
   4: var bootstrapper = new Bootstrapper(container);
   5: bootstrapper.Start();
   6:  
   7: DependencyResolver.SetResolver(new UnityDependencyResolver(container));

Après l’exécution de l’application, le contrôleur est créé par notre  UnityDependencyResolver et reçoit automatiquement en paramètre l’instance du service enregistré par Unity :

ExécutionIDependencyResolver

Et comme c’est à prévoir il n’y a plus d’erreur :

Screen

2. Implémentation de IServiceLocator

Une autre manière d’enregistrer notre containeur DI auprès du Framework MVC 3 et l’implémentation de IServiceLocator ou de ServiceLocatorImplBase qui implémente tout simplement l’interface en question. Cette interface ainsi que la classe de base se trouvent dans la librairie Microsoft.Practices.ServiceLocation. Pour la question de facilité, il vaut mieux dériver de ServiceLocatorImplBase  ce que nous faisons dans la classe UnityServiceLocator que voici:

   1: public class UnityServiceLocator : ServiceLocatorImplBase
   2: {
   3:     private readonly IUnityContainer _container;
   4:  
   5:     public UnityServiceLocator(IUnityContainer container)
   6:     {
   7:         _container = container;
   8:     }
   9:  
  10:     protected override object DoGetInstance(Type serviceType, string key)
  11:     {
  12:         object instance;
  13:  
  14:         try
  15:         {
  16:             instance = _container.Resolve(serviceType, key);
  17:         }
  18:         catch
  19:         {
  20:             instance = null;
  21:         }
  22:  
  23:         return instance;
  24:     }
  25:  
  26:     protected override IEnumerable<object> DoGetAllInstances(Type serviceType)
  27:     {
  28:         IEnumerable<object> instances;
  29:  
  30:         try
  31:         {
  32:             instances = _container.ResolveAll(serviceType);
  33:         }
  34:         catch
  35:         {
  36:             instances = new object[] { };
  37:         }
  38:  
  39:         return instances;
  40:     }
  41: }

Nous devons surcharger deux méthodes :

  • DoGetInstance(Type serviceType, string key) pour la résolution de services d’enregistrement unique.
  • DoGetAllInstances(Type serviceType) pour la résolution de services d’enregistrement multiple.

Ensuite c’est similaire à l’implémentation de IDependencyResolver. En paramètre du constructeur de notre classe UnityServiceLocator nous lui passons une instance de notre containeur Unity. Ce qui est important de noter ce que la méthode object DoGetInstance(Type serviceType, string key) doit retourner null si le service ne peut pas être résolu par Unity, et la méthode IEnumerable<object> DoGetAllInstances(Type serviceType) doit retourner une collection vide. Si ce n’est pas respecté vous allez avoir une exception au run-time.

Il ne nous reste que d’enregistrer notre IServiceLocator dans le fichier Global.asax avec la méthode DependencyResolver.SetResolver():

   1: IUnityContainer container = new UnityContainer();
   2:  
   3: // enregistrer les types dans unity (ex: IOrderService, OrderService)
   4: var bootstrapper = new Bootstrapper(container);
   5: bootstrapper.Start();
   6:  
   7: DependencyResolver.SetResolver(new UnityServiceLocator(container));

Une petite explication s’impose. Si on regarde la signature de la méthode qui permet de réaliser l’enregistrement de IServiceLocator on ne voit la méthode qui prend en paramètre le type object :

   1: public static void SetResolver(object commonServiceLocator)

On pourrait se poser la question pourquoi alors il faut implémenter IServiceLocator alors que le paramètre n’est qu’un type object? Le choix de l’équipe MVC était judicieux et ceci pour ne pas créer la dépendance sur la librairie Microsoft.Practices.ServiceLocation où l’interface est définie et le Framework MVC 3 ! Mais si on regarde l’implémentation de la méthode public static void SetResolver(object commonServiceLocator) de DependencyResolver, nous découvrons que la Reflection est utilisée pour inspecter notre objet afin de vérifier que les méthodes GetInstance et GetAllInstances sont présentes (lignes 7 et 8 de l’extrait de code ci dessous). Voici un extrait de ce code :

   1: public void InnerSetResolver(object commonServiceLocator) {
   2:     if (commonServiceLocator == null) {
   3:         throw new ArgumentNullException("commonServiceLocator");
   4:     }
   5:     
   6:     Type locatorType = commonServiceLocator.GetType();
   7:     MethodInfo getInstance = locatorType.GetMethod("GetInstance", new[] { typeof(Type) });
   8:     MethodInfo getInstances = locatorType.GetMethod("GetAllInstances", new[] { typeof(Type) });
   9:     
  10:     if (getInstance == null ||
  11:         getInstance.ReturnType != typeof(object) ||
  12:         getInstances == null ||
  13:         getInstances.ReturnType != typeof(IEnumerable<object>)) {
  14:         throw new ArgumentException(
  15:             String.Format(
  16:                 CultureInfo.CurrentCulture,
  17:                 MvcResources.DependencyResolver_DoesNotImplementICommonServiceLocator,
  18:                 locatorType.FullName
  19:             ),
  20:             "commonServiceLocator"
  21:         );
  22:     }

Si vous avez fait attention alors vous pourriez-vous poser une autre question. Comment ceci peut-marcher puisque nous n’avons pas surchargé ces méthodes dans la classe UnityServiceLocator  mais DoGetInstance et DoGetAllInstance? Pas d’inquiétude. Un coup de Reflector permet de vérifier qu’en fait nos méthodes font bien appel en interne aux méthodes dont DependencyResolver a besoin :

Reflector

Donc tout vas pour le mieux !

Si on exécute l’application tout marche comme prévu !

3. Enregistrement par les délégués.

La dernière manière de permettre l’injection de dépendance est passer par les fonction ad-hoc. Ceci peut être réalisé directement dans Global.asax :

   1: IUnityContainer container = new UnityContainer();
   2:  
   3: // enregistrer les types dans unity (ex: IOrderService, OrderService)
   4: var bootstrapper = new Bootstrapper(container);
   5: bootstrapper.Start();
   6:  
   7: DependencyResolver.SetResolver(
   8:                 t => (t.IsClass && !t.IsAbstract) || container.IsRegistered(t)? container.Resolve(t) : null,
   9:                 t => (t.IsClass && !t.IsAbstract) || container.IsRegistered(t)? container.ResolveAll(t) : new object[] { });

Quel méthode d’enregistrement utiliser ?

Je dirai que cela n’a pas d’importance car de toute façon au final tous ces enregistrement sont transformés en IDependencyResolver par le Framework MVC 3. Donc ce que vous manipulez au final c’est bien IDependencyResolver. Il est intéressant de noter que vous pouvez utiliser la classe DependencyResolver en tant que service locator classique (mais à éviter car l’utilisation de service locator est un anti-pattern). Dans l’exemple ci-dessous je récupère l’instance de mon service en utilisant cette technique :

DependencyResolverLocation

What Next ?

Dans le prochain post je parlerai en détails de IControllerActivator.

 

A bientôt Sourire

 

// Thomas