L'injection de dépendances, et les avantages que cette technique apporte, devrait normalement être un concept connu de la plupart des développeurs. Pour un bref rappel, cette technique consiste en la délégation de la résolution d'une dépendance par un composant à l'exécution, et non plus à la compilation. Ceci permet de diminuer le couplage entre les différentes entités de l'application, et donc, par exemple, de faciliter les tests unitaires. Cette technique et les logiciels qui s'y rapportent ont déjà été évoqués dans cet article : Injection de dépendances en .NET.
Ce billet fait suite à la série sur les nouveautés d'ASP.NET MVC 3 écrit conjointement avec Philippe Viallate. Vous pouvez consulter les précédents billets :
- [ASP.NET MVC] Nouveautés MVC 3 Part 1 - Améliorations dans Visual Studio 2010
- [ASP.NET MVC] Nouveautés MVC 3 Part 2 - Améliorations pour la Validation
- [ASP.NET MVC] Nouveautés MVC 3 Part 3 - Global Action FIlters
Dans les versions précédentes du Framework MVC, il n'était pas particulièrement plus aisé de mettre en place l'injection de dépendances qu'avec Web Forms (ou Winform), ce qui était navrant, vu que MVC est très orienté test unitaires, et que l'injection de dépendance est très utilisé dans ce cadre. Il était en effet nécessaire, pour pouvoir injecter nos services, d'ajouter une certaine quantité de code de « plomberie » de façon à pouvoir gérer ce scénario de façon relativement transparente.
La version 3 corrige cette lacune, et l'injection de dépendance fait maintenant partie des fonctionnalités fondamentales du Framework. En effet, le Framework inclut dorénavant une localisation unique dans laquelle les dépendances à être résolues vont être cherchées. Cette fonctionnalité se rapproche du patron de conception Service Locator.
Attention, MVC3 ne fournit pas de Framework d'injection de dépendances, mais offre des points d'extensions pour permettre de « brancher » facilement un Framework existant.
Interface IDependencyResolver
Cette nouvelle interface ajoute un niveau d'indirection au-dessus des autres Frameworks, dans le but de standardiser leur configuration. L'interface IDependencyResolver est tout ce qu'il y'à de plus simple. Elle se présente en effet ainsi :
namespace System.Web.Mvc {
public interface IDependencyResolver {
object GetService(Type serviceType);
IEnumerable<object> GetServices(Type serviceType);
}
}
Pour que MVC 3 utilise un Framework d'injection de dépendance, il nous suffit donc de créer (ou de réutiliser, il y'à de fortes chances que la plupart de ces Frameworks proposent une classe de ce type très vite) une classe implémentant l'interface, et d'appeler, au niveau de la méthode Application_Start, la méthode DependencyResolver.SetResolver.
Nous allons, pour l'exemple, utiliser la classe StructureMapDependencyResolver décrite par Steve Smith :
public class StructureMapDependencyResolver : IDependencyResolver
{
public StructureMapDependencyResolver(IContainer container)
{
_container = container;
}
public object GetService(Type serviceType)
{
if (serviceType.IsAbstract || serviceType.IsInterface)
{
return _container.TryGetInstance(serviceType);
}
return _container.GetInstance(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
return _container.GetAllInstances<object>()
.Where(s => s.GetType() == serviceType);
}
private readonly IContainer _container;
}
Maintenant que nous avons notre classe StructureMapDependencyResolver, il ne nous reste plus qu'à ajouter cette ligne :
DependencyResolver.SetResolver(new StructureMapDependencyResolver(new Container())) ;
Pour bénéficier de l'injection de dépendance de façon quasiment transparente.
Utilisation du DependencyResolver
En effet, pour revenir à nos exemples précédents, nous allons légèrement refactoriser notre code, de façon à ce que notre classe UserManager soit injectée depuis le DependencyResolver.
public class UserManager
{
private readonly UserRepository _repository;
public bool ContainsLogin (string login)
{
return _repository.Any(user => user.Login.Equals(login));
}
}
On va donc commencer par définir notre interface IUserManager :
public inteface IUserManager{
public bool ContainsLogin();
}
Puis modifier la classe UserManager pour lui faire implémenter l'interface IUserManager :
public class UserManager : IUserManager
{
// le reste ne change pas
}
On va ensuite, configurer notre conteneur, de la façon suivante :
IContainer container = new Container(x =>
{
x.For<IUserManager>().Use<UserManager>();
});
DependencyResolver.SetResolver(new StructureMapDependencyResolver(container));
Notre conteneur est prêt, il nous faut maintenant modifier notre classe UserController, de façon à passer en paramètre une interface IUserManager, et modifier la méthode CheckLogin, qui va utiliser cet objet.
public class UserController : Controller
{
private readonly IUserManager _manager;
public UserController(IUserManager manager)
{
_manager = manager;
}
public ActionResult CheckLogin(string login)
{
return Json(!_manager.ContainsLogin(login), JsonRequestBehavior.AllowGet);
}
// reste de la classe
}
Et c'est tout ! Au moment où le contrôleur est appelé, le Framework va appeler le DependencyResolver, et récupérer le type concret UserManager.
Dans le cas que l'on vient de voir, le Framework nous aide, en ajoutant les appels nécessaires de façon silencieuse. Il y'à cependant des cas où l'on voudra appeler directement le DependencyResolver, ce qui se fait simplement en appelant la méthode GetService du DependencyResolver actif :
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (DependencyResolver.Current.GetService<IUserManager>().ContainsLogin(Login))
yield return new ValidationResult("Ce login existe déjà");
}
Nouvelles interfaces IControllerActivator et IViewPageActivator
Toujours dans l'idée d'ajouter des possibilités d'extensions du Framework, deux nouvelles interfaces ont été ajoutées. L'idée de ces interfaces est de permettre aux développeurs de gérer très précisément la façon dont un contrôleur (ou une vue pour IViewPageActivator) est instancié grâce à l'injection de dépendances.
Ces deux interfaces sont relativement simples, vu qu'elles demandent simplement d'implémenter une méthode Create.
public interface IControllerActivator
{
IController Create(RequestContext requestContext, Type controllerType);
}
public interface IViewPageActivator
{
object Create(ControllerContext controllerContext, Type type);
}
Implémenter ces classes nous permettra de controler le scénario de résolution de dépendance en se basant sur le DependencyResolver. On pourrait éventuellement vouloir ajouter des logs ou tout autre scénario alternatif... On pourrait par exemple faire ceci :
public class StructureMapControllerActivator : IControllerActivator
{
public IController Create(RequestContext requestContext, Type controllerType){
// logger l'appel à Create
return (IController)DependencyResolver.Current.GetService<controllerType>();
}
}
public class StructureMapViewPageActivator : IViewPageActivator
{
public object Create(ControllerContext controllerContext, Type type){
// logger l'appel à Create
return DependencyResolver.Current.GetService<type>();
}
}
Il nous reste néanmoins à enregistrer nos activateurs dans le DependencyResolver, de la façon suivante :
IContainer container = new Container(x =>
{
x.For<IViewPageActivator>().Use<StructureMapViewPageActivator>();
x.For<IControllerActivator>().Use<StructureMapControllerActivator>();
});
DependencyResolver.SetResolver(new StructureMapDependencyResolver(container));