Ninject, from Zero to Hero – Part 4

Depuis l’émergence du mouvement Craft, le Clean Code et les principes SOLID sont à la mode. Tout développeur qui se respecte doit produire du code maintenable, testable et réutilisable. Dans cet article, nous allons vous présenter Ninject, un outil dont le but est de faciliter la production d’un tel code.
Suite du troisième article sur Ninject dans lequel nous avons vu comment faire des injections conditionnées. Nous allons cette fois-ci voir comment avoir une utilisation plus avancée de Ninject.
Exemple d’utilisation avancée : implémentez votre propre ActionFilterAttribute
ASP.NET MVC offre un mécanisme très utile de décorateur via attribut : les filtres. Ces attributs servent à lancer des traitements avant ou après le déroulement des actions (méthodes publiques des controlleurs) qu’ils décorent. Le système est comparable aux triggers du monde des bases de données.
L’attribute OutputCache, par exemple, sert à mettre en cache le résultat d’une action pour une durée donnée.
using System; using System.Web; using System.Web.Mvc; namespace MvcApplication1.Controllers { public class DataController : Controller { [OutputCache(Duration=10)] public string Index() { return DateTime.Now.ToString("T"); } } }
Le code obtenu est beaucoup plus simple que celui qu’il aurait fallu écrire sans l’attribut.
Seulement, les filtres sont une spécificité d’ASP.NET MVC et ne sont pas disponibles dans les applications consoles ou WPF. Heureusement, il est très facile d’implémenter un mécanisme similaire à l’aide de Ninject et de deux extensions : Ninject.Extensions.Interception et Ninject.Extensions.Interception.DynamicProxy.
Nous allons voir comment procéder en créant un système de traçabilité des accès aux méthodes sensibles de nos applications.
Traçabilité des accès à une méthode critique
Depuis le Gestionnaire de Package Nuget, installez Ninject.Extensions.Interception.DynamicProxy. Nuget vous propose d’installer les dépendances, dont Ninject.Extensions.Interception
Imaginons maintenant que nous ayons un service offrant des opérations sensibles.
public class SensitiveService { public void SensitiveOperation() { Console.WriteLine("Armement d'un missile"); } public void OtherSensitiveOperation() { Console.WriteLine("Accès aux informations personnelles du Président..."); } }
Ce que nous aimerions, c’est garder une trace des utilisateurs qui ont déclenché l’exécution de ces opérations. On pourrait, bien entendu, ajouter une ligne de log dans chaque méthode, mais la duplication de code se ferait sentir rapidement à mesure que le nombre d’opérations sensibles augmenterait.
Nous allons donc créer un décorateur qui se chargera de tracer les accès aux méthodes critiques.
1ere étape : L’interception
Il nous faut tout d’abord créer un intercepteur, c’est-à-dire une classe qui lancera un traitement au moment voulu, dans notre cas, avant l’exécution d’une méthode sensible. Ninject Interception fournit une interface prévue à cet effet : IInterceptor. Cette interface offre une seule méthode : Intercept
using Ninject.Extensions.Interception; namespace GreetingsGenerator { public class SensitiveOperationAccessInterceptor : IInterceptor { private ILogger _logger; private string _userName; public SensitiveOperationAccessInterceptor(ILogger logger, string userName) { _logger = logger; _userName = userName; } public void Intercept(IInvocation invocation) { var methodName = invocation.Request.Method.Name; _logger.Log(string.Format("L'utilisateur {0} a effectué \ une opération sensible: [{1}]", _userName, methodName)); invocation.Proceed(); } } }
Notre intercepteur va simplement logger deux informations : le nom de la méthode sensible et le nom de l’utilisateur ayant demandé l’opération.
Lorsqu’une méthode sensible s’exécutera, la méthode Intercept de l’intercepteur sera exécutée. Le contexte de l’événement intercepté est représenté par le paramètre invocation et sa propriété Request. C’est cette dernière qui va nous fournir le nom de la méthode critique invoquée.
Le logger et le nom de l’utilisateur sont passés par construction et via Ninject, nous allons voir comment un peu plus loin.
2ème étape : Création de l’attribut
Maintenant, il nous faut l’attribut qui va nous servir à décorer nos méthodes. Là encore, l’extension d’interception de Ninject nous apporte le nécessaire : le type InterceptAttribute.
using Ninject; using Ninject.Extensions.Interception; using Ninject.Extensions.Interception.Attributes; using Ninject.Extensions.Interception.Request; namespace GreetingsGenerator { public class TraceSensitiveOperationAccessAttribute : InterceptAttribute { public override IInterceptor CreateInterceptor(IProxyRequest request) { return request.Context.Kernel.Get<SensitiveOperationAccessInterceptor>(); } } }
InterceptAttribute expose une méthode abstraite CreateInterceptor, qui renvoie une instance de l’intercepteur à utiliser avant l’exécution de la méthode décorée. Le paramètre request représente le contexte dans lequel la méthode est invoquée. Il nous permet d’obtenir une référence au kernel de Ninject. Sans surprise, l’intercepteur qui nous intéresse est celui que nous avons créé précédemment et qui va s’occuper de tracer les accès aux opérations sensibles.
3ème étape : les coutures
Si vous avez fait attention, vous avez remarqué que lors de la création de l’intercepteur, nous ne fournissons pas les paramètres de construction attendus, à savoir le logger et le userName. C’est dans notre racine de composition qu’a lieu la magie….
static void Main(string[] args) { using (var kernel = new StandardKernel()) { kernel.Bind<SensitiveOperationAccessInterceptor>() .ToSelf() .WithConstructorArgument("userName", "John Smith"); kernel.Bind<ILogger>().To<ConsoleLogger>(); var sensitiveService = kernel.Get<SensitiveService>(); sensitiveService.SensitiveOperation(); sensitiveService.OtherSensitiveOperation(); Console.Read(); } }
Voyons le code pas à pas
D’abord, nous indiquons à Ninject qu’à chaque fois qu’une instance de SensitiveOperationAccessInterceptor lui est demandée, il doit en créer une avec le paramètre userName égal à « John Smith ».
kernel.Bind<SensitiveOperationAccessInterceptor>() .ToSelf() .WithConstructorArgument("userName", "John Smith");
Dans la pratique, le nom de l’utilisateur proviendrait bien évidemment du contexte d’exécution de l’application (en utilisant par exemple un Objet de Contexte).
Ensuite, nous demandons à Ninject de fournir une instance de ConsoleLogger à chaque fois qu’une instance de ILogger est nécessaire.
kernel.Bind<ILogger>().To<ConsoleLogger>();
Ces deux instructions nous ont suffit à paramétrer Ninject pour qu’il fournisse l’instance d’intercepteur qui convient dans la méthode CreateInterceptor de notre attribut.
Il ne nous reste plus qu’à récupérer l’instance du notre service à l’aide de Ninject. Attention, si vous instanciez le service vous-même à l’aide du mot-clé new, vous brisez la chaîne d’injection et le système ne fonctionnera pas.
var sensitiveService = kernel.Get<SensitiveService>();
Il ne nous reste plus qu’à décorer nos méthodes sensibles… Notez bien que les méthodes décorées de la sorte doivent être virtuelles pour que l’interception fonctionne.
public class SensitiveService { [TraceSensitiveOperationAccess] public virtual void SensitiveOperation() { Console.WriteLine("Armement d'un missile"); } [TraceSensitiveOperationAccess] public virtual void OtherSensitiveOperation() { Console.WriteLine("Accès aux informations personnelles du Président..."); } }