Injection de dépendances au sein de Windows Activation Services avec Unity

Depuis quelques années, le concept d’IoC ou d’inversion de contrôle s’est répandu au sein des communautés de développeurs. L’IoC est un patron d’architecture, où le flot d’exécution n’est plus entièrement déterminé par l’application elle-même, mais par un framework sous-jacent. Cette notion est souvent confondue avec celle de l’injection de dépendances (DI pour dependency injection, qui correspond au D des principes SOLID), qui en est un sous-ensemble.

WAS

Je ne vais pas m’étendre sur le sujet IoC vs injection de dépendances. Généralement, vous utilisez un framework IoC tel que Unity, MEF, NInject, Castle Windsor, Spring.Net etc. Ce framework met à votre disposition ce qu’on appelle classiquement «conteneur IoC», et via ce conteneur, vous allez pouvoir récupérer une instance de votre objet. Oh Génie, fournis moi une instance de IQuelqueChose…Et hop… la voilà ! this.Container.Resolve<IQuelqueChose>();.

Cependant, la plupart des frameworks vous permettent de récupérer automatiquement les instances enregistrées dans le conteneur, par constructeur ou par propriétés. L’injection de dépendances par constructeur est très simple : si votre instance est fournie par le conteneur, alors vous pouvez fournir autant de paramètres que vous le souhaitez au niveau de la définition du constructeur. Chaque paramètre sera résolu, si cela est possible.

L’injection de dépendances par propriétés utilise généralement une décoration («Dependency» sous Unity, ou «Import» sous MEF). Le choix de l’une ou l’autre approche dépendra de plusieurs facteurs :

  • les paramètres sont ils vitaux à votre objet ?
  • combien de paramètres sont présents dans votre constructeur ?

Si un paramètre est vital à votre objet, il est alors inutile de poursuivre la construction de votre objet sans ce paramètre. Toutefois, si vous avez déjà trop de paramètres dans votre constructeur, en ajouter encore un en rendrait le code difficilement maintenable. Une solution serait de passer le conteneur IoC en paramètre, puis d’utiliser ce conteneur, afin de résoudre les n paramètres restants.

En pratique, comment faire ?
Il faut :

  • référencer les dlls de votre framework IoC;
  • disposer de ce que l’on appelle un conteneur IoC que l’on instanciera à l’aide d’un «new»;
  • enregistrer des correspondances entre une interface et une implémentation, ainsi que la durée de vie des objets instanciés (singleton ou nouvelle instanciation à chaque résolution étant les deux modes les plus répandus);
  • réclamer l’instance d’une des interfaces déjà enregistrée, ou celle d’une classe concrète. Ainsi, vous n’aurez pas à spécifier tous les paramètres de votre constructeur pour en obtenir une instance.

On peut bien entendu configurer le conteneur via un fichier de configuration ou bien par code.

Pour une application Console avec Unity

J’ai enregistré dans mon conteneur le mapping entre IMyInterfaceSingleton et son implémentation MyImplementationSingleton, en tant que singleton. Je fais de même avec IMyInterfaceNotSingleton et son implémentation. Je demande ensuite une instance de MyClassWithComplexConstructor, laquelle prend IMyInterfaceSingleton en paramètre de son constructeur. La résolution se fait automatiquement, car nous avons déclaré le mapping. De même, on constate que la propriété MyInterfaceNotSingleton est décorée par «Dependency», et celle-ci sera résolue lors du Console.WriteLine() ;.

WAS2

On constate que la propriété singleton, pour les deux objets de type MyClassWithComplexConstructor, est bien un singleton (affichage du premier paramètre), alors que le second, ne l’est pas. Et là, vous vous dites désormais que vous avez tout compris à l’IoC ! On vous confie une application WPF, vous faites votre injection de dépendances sans vous poser de réelles questions. Dans le fichier App.xaml, retirez la propriété StartUpUri, et dans le code behind, surchargez la méthode OnStartUp.

   1: public partial class App : Application

   2: {

   3:     protected override void OnStartup(StartupEventArgs e)

   4:     {

   5:         base.OnStartup(e);

   6:

   7:         //Mettez ici le code pour instancier votre conteneur, puis instancier votre fenêtre principale via ce dernier, suivi d’un Show()

   8:     }

   9: }

C’est magique ! Vous êtes le roi du monde… ou presque ! Puis vous vous lancez dans un projet ASP.Net MVC4 et là, les choses se gâtent ! Au niveau du global.asax, après avoir configuré les routes, les bundles et l’authentification, vous pouvez faire appel au code suivant dans la méthode Application_Start() : Bootstrapper.Initialize();.

   1: public static class Bootstrapper

   2: {

   3:     public static IUnityContainer Initialize()

   4:     {

   5:       var container = BuildUnityContainer();

   6:

   7:       DependencyResolver.SetResolver(new UnityDependencyResolver(container));

   8:

   9:       return container;

  10:     }

  11:

  12:     private static IUnityContainer BuildUnityContainer()

  13:     {

  14:       var container = new UnityContainer();

  15:

  16:       // register all your components with the container here

  17:       // it is NOT necessary to register your controllers

  18:

  19:       // e.g. container.RegisterType<ITestService, TestService>();    

  20:       RegisterTypes(container);

  21:

  22:       return container;

  23:     }

  24:

  25:     public static void RegisterTypes(IUnityContainer container)

  26:     {

  27:         var repo = new ProductRepository();

  28:         container.RegisterType(typeof (IProductRepository), typeof (ProductRepository), new ContainerControlledLifetimeManager());

  29:     }

  30:  }

Ouf ! Ca marche encore ! Et puis un jour, vous tombez sur un projet dans lequel on utilise uniquement WAS. WAS est l’acronyme de Windows Activation Services. Il s’agit d’utiliser IIS, afin d’héberger vos Webservices WCF. C’est d’ailleurs le cas d’utilisation le plus courant. En effet, pourquoi s’embêter à héberger son service WCF dans une application Console ou un service Windows ? Bien sûr, il y a des raisons particulières pour lesquelles ont choisit ce genre d’hébergement (self hosted), mais la plupart du temps, il vaut mieux préférer WAS en raison de :

  • son interface de management (IIS Manager et AppFabric Server si vous l’avez installé);
  • sa scalabilité horizontale de IIS (fermes de serveurs, load balanceurs etc.);
  • la gestion du cycle de vie des services (pools, instanciation de services etc.);
  • etc.

Toutefois, WAS n’utilise pas les modules «ASP.Net» mais uniquement WCF. Dans le cas d’une communication purement http, on pourrait envisager d’utiliser la même technique que précédemment. Cependant, WCF permet l’utilisation de plusieurs protocoles non http (donc, pas de global.asax), et il vaut mieux s’interfacer ailleurs pour faire notre injection. Dans WAS, on n’a pas la main quant à l’ouverture de l’hôte de services. Une solution possible consisterait à mettre tout notre code relatif à l’injection au niveau du constructeur du service. Toutefois, ce choix comporte quelques inconvénients :

  • à chaque fois qu’une instance de service est créée, tout le code d’initialisation de l’IoC est re-exécuté, ce qui peut être pénalisant en cas de nombreux appels;
  • on pourrait décider de paramétrer notre service WCF en tant que singleton, mais en contrepartie, si plusieurs appels simultanés sont effectués, il n’y aura pas de nouvelles instances de service.

Voici pour ma part, une solution que je trouve plutôt élégante. Elle utilise la notion de service Host Factory. Qu’est qu’un service Host Factory ? C’est un point d’extension de WCF permettant d’effectuer quelques traitements lors de l’opération OnOpening de l’hôte WCF : http://msdn.microsoft.com/fr-fr/library/aa702697.aspx. Par exemple, si nous utilisons Unity comme framework d’IoC, nous avons besoin de 4 classes de base pour arriver à notre but :

  • AbstractUnityServiceHostFactory;
  • UnityServiceHost;
  • UnityInstanceProvider;
  • UnityServiceBehavior.

La classe AbstractUnityServiceHostFactory se décline en deux versions, selon les flux :

  • en SOAP : utilisez ServiceHostFactory;
  • en JSON : utilisez WebServiceHostFactory.

Le reste reste est identique. Cette classe est marquée Abstract, car elle vous permettra d’enregistrer vos mappings via un héritage. Vous devrez donc créer votre propre service Host Factory, héritant de cette AbstractUnityServiceHostFactory.

   1: public abstract class AbstractUnityServiceHostFactory : ServiceHostFactory

   2: {

   3:     private IUnityContainer _Container;

   4:     protected IUnityContainer Container

   5:     {

   6:         get { return this._Container; }

   7:         set { this._Container = value; }

   8:     }

   9:

  10:     protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)

  11:     {

  12:         var serviceHost = new UnityServiceHost(serviceType, baseAddresses);

  13:         this.Container = serviceHost.Container;

  14:

  15:         //Code pour configurer votre container           

  16:

  17:         return serviceHost;

  18:     }

  19: }

La classe UnityServiceHost comporte également deux variantes en fonction des flux : ServiceHost ou WebServiceHost.

   1: public class UnityServiceHost : ServiceHost

   2: {

   3:     /// <summary>

   4:     /// Gets the default <see cref="IUnityContainer"/> for the application.

   5:     /// </summary>

   6:     /// <value>The default <see cref="IUnityContainer"/> instance.</value>

   7:     private IUnityContainer _Container;

   8:     public IUnityContainer Container

   9:     {

  10:         get { return this._Container; }

  11:         set { this._Container = value; }

  12:     }

  13:

  14:     public UnityServiceHost()

  15:         : base()

  16:     {

  17:         this.Container = new UnityContainer();

  18:     }

  19:

  20:     public UnityServiceHost(Type serviceType, params Uri[] baseAddresses)

  21:         : base(serviceType, baseAddresses)

  22:     {

  23:         this.Container = new UnityContainer();

  24:     }

  25:

  26:     protected override void OnOpening()

  27:     {

  28:         base.OnOpening();

  29:         if (this.Description.Behaviors.Find<UnityServiceBehavior>() == null)

  30:             this.Description.Behaviors.Add(new UnityServiceBehavior(this.Container));

  31:     }

  32: }

La classe UnityInstanceProvider est la suivante (à recopier telle quelle) :

   1: public class UnityInstanceProvider : IInstanceProvider

   2: {

   3:     private IUnityContainer _Container;

   4:     public IUnityContainer Container

   5:     {

   6:         get { return this._Container; }

   7:         set { this._Container = value; }

   8:     }

   9:

  10:     private Type _ServiceType;

  11:     public Type ServiceType

  12:     {

  13:         get { return this._ServiceType; }

  14:         set { this._ServiceType = value; }

  15:     }

  16:

  17:     public UnityInstanceProvider()

  18:         : this(null)

  19:     {

  20:

  21:     }

  22:

  23:     public UnityInstanceProvider(Type type)

  24:     {

  25:         this._ServiceType = type;

  26:         this._Container = new UnityContainer();

  27:     }

  28:

  29:     // Get Service instace via unity container        

  30:     public object GetInstance(InstanceContext instanceContext, Message message)

  31:     {

  32:         return this._Container.Resolve(ServiceType);

  33:     }

  34:

  35:     public object GetInstance(InstanceContext instanceContext)

  36:     {

  37:         return this.GetInstance(instanceContext, null);

  38:     }

  39:

  40:     public void ReleaseInstance(InstanceContext instanceContext, object instance)

  41:     {

  42:

  43:     }

  44: }

Idem pour la classe UnityServiceBehavior.

   1: public class UnityServiceBehavior : IServiceBehavior

   2: {

   3:     private UnityInstanceProvider _InstanceProvider;

   4:     public UnityInstanceProvider InstanceProvider

   5:     {

   6:         get { return this._InstanceProvider; }

   7:         set { this._InstanceProvider = value; }

   8:     }

   9:

  10:     public UnityServiceBehavior()

  11:     {

  12:         this.InstanceProvider = new UnityInstanceProvider();

  13:     }

  14:

  15:     public UnityServiceBehavior(IUnityContainer unity)

  16:     {

  17:         this.InstanceProvider = new UnityInstanceProvider();

  18:         this.InstanceProvider.Container = unity;

  19:     }

  20:

  21:     public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)

  22:     {

  23:

  24:     }

  25:

  26:     public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)

  27:     {

  28:         foreach (var cdb in serviceHostBase.ChannelDispatchers)

  29:         {

  30:             var cd = cdb as ChannelDispatcher;

  31:             if (cd != null)

  32:             {

  33:                 foreach (var ed in cd.Endpoints)

  34:                 {

  35:                     this.InstanceProvider.ServiceType = serviceDescription.ServiceType;

  36:                     ed.DispatchRuntime.InstanceProvider = InstanceProvider;

  37:                 }

  38:             }

  39:         }

  40:     }

  41:

  42:     public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)

  43:     {

  44:

  45:     }

  46: }

Désormais, il ne vous reste plus qu’à déclarer correctement votre service WCF :

Exemple de fichier .svc :

   1: <%@ ServiceHost Language="C#" Debug="true" Service="Server.Wcf.SwiftBase.SwiftBaseIdentityService" Factory="Server.Wcf.SwiftBase.SwiftBaseUnityServiceHostFactory, Server.Wcf.SwiftBase" %>

Pour rappel, le nom de votre UnityServiceHostFactory doit comporter le namespace.

Désormais, au sein de vos instances de services, vous pouvez profiter des joies de l’injection de dépendances ! Et votre conteneur n’est instancié qu’une seule fois !

En résumé, la solution consiste à :

  • utiliser l’extension WCF Host Factory (http://msdn.microsoft.com/fr-fr/library/aa702697.aspx);
  • implémenter les 4 classes de bases : AbstractUnityServiceHostFactory (décliné en ServiceHostFactory pour un flux SOAP ou WebServiceHostFactory pour JSON ), UnityServiceHost (décliné en ServiceHost ou WebServiceHost en fonction du flux), UnityInstanceProvider et UnityServiceBehavior;
  • créer un projet de bibliothèque WCF et créer son propre Service Host Factory en créant une classe dérivant de AbstractUnityServiceHostFactory;
  • éditer le fichier .svc au sein de l’hôte de service pour référencer le service à partir de la bibliothèque WCF.
Share
Patrick LEGRAND
Patrick LEGRAND

1667

Leave a Reply

Your email address will not be published. Required fields are marked *