Umbraco: Event handlers pipeline via interception (Unity et PIAB)

Précédemment, j’ai décrit une approche pour mettre en place un pipeline d’observateurs d’événements (observation des Event handlers d’Umbraco). Cet article présente comment étendre cette approche aux Action handlers d’Umbraco.


Cet article s’inscrit dans une suite:

Note importante : ces trois articles sont adaptés à Umbraco 4.7.

Rappels

La mise en place du pipeline suit trois étapes:

  1. Mise en oeuvre d’un proxy (objet interceptable).
  2. Interception du proxy.
  3. Injection d’un pipeline de « call handlers ».

Seules les deux premières étapes sont décrites dans cet article car l’injection du pipeline se fait de façon strictement identique qu’avec les Event handlers: grâce aux matching rules et aux call handlers.

Action proxy et interception de celle-ci

Le proxy des Event handlers était représenté par un seul objet (dérivant de ApplicationBase) qui s’inscrivait aux événements à intercepter. Les matching rules identifiaient l’événement à intercepter à partir du nom de la méthode inscrite par ce proxy. Pour les Action handlers, nous créerons un proxy par action. Rappelons qu’un Action handler dérive de IActionHandler. Les matching rules se baseront sur le type de l’action interceptée (qui dérive de IAction). Comme la logique est la même pour toutes les actions, nous nous baserons sur une classe proxy abstraite.

Voici notre proxy composite, en plusieurs parties:

using System;

namespace Umbraco.Interception.Actions
{
 public abstract class ActionProxyBase
 {
  internal const string TARGET_METHOD = "Invoke";

  internal umbraco.interfaces.IAction Action
  {
      get;
      set;
  }

  public virtual bool Invoke(umbraco.cms.businesslogic.web.Document documentObject)
  {
   return true;
  }
 }
}

La classe ActionProxyBase définit simplement une propriété de type IAction et une méthode virtuelle Invoke. Rappelons que cette méthode doit être virtuelle pour être interceptable. La méthode interceptable n’a aucune action, sa seule fonction étant d’être, précisément, interceptée.


using System;
using System.Reflection;

namespace Umbraco.Interception.Actions
{
 public class ActionProxy<T> : ActionProxyBase where T : umbraco.interfaces.IAction
 {
  public ActionProxy()
  {
      Type type = typeof(T);
      PropertyInfo instance = type.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static);
      if (instance == null)
   base.Action = Activator.CreateInstance(type) as umbraco.interfaces.IAction;
      else
   base.Action = instance.GetValue(null, null) as umbraco.interfaces.IAction;
  }
 }
}

La classe ActionProxy<T> est notre véritable « proxy de base » (nous employons le terme « base » bien qu’il ne soit pas abstrait, la suite sera plus claire). C’est précisément cette classe qui sera interceptée avec Unity. Le constructeur utilise le type générique fourni (qui doit implémenter IAction) pour créer une instance à affecter à la propriété ActionProxyBase.Action. Le code pour créer cette instance est repris d’Umbraco (méthode RegisterIActions du fichier Action.cs). Cette implémentation est donc fiable (pas d’incertitudes sur la façon d’instancier ces objets).

Voici maintenant l’action qui se basera sur notre proxy. Nous atteignons l’étape d’interception décrite dans nos trois étapes, au début de cet article:


using System;

using Microsoft.Practices.Unity; // notamment pour la méthode d'extension IUnityContainer.AddNewExtension().
using Microsoft.Practices.Unity.InterceptionExtension;

using Microsoft.Practices.EnterpriseLibrary.Common.Configuration.ContainerModel.Unity; // pour UnityContainerConfigurator 
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration; // pour EnterpriseLibraryContainer et ConfigurationSourceFactory

namespace Bollore.UmbracoInterception.Actions
{
    public abstract class ActionInterceptorBase : umbraco.BusinessLogic.Actions.IActionHandler
    {
        private ActionProxyBase _proxy;

        public ActionInterceptorBase(ActionProxyBase proxy)
        {
            if (proxy == null)
                throw new ArgumentNullException("proxy");
            _proxy = proxy;
        }

        protected static T GetInterceptor<T>() where T : new()
        {
            IUnityContainer container = new UnityContainer();

            var configurator = new UnityContainerConfigurator(container);
            EnterpriseLibraryContainer.ConfigureContainer(configurator, ConfigurationSourceFactory.Create());

            container.RegisterType<T>
                    (
                    new Interceptor<VirtualMethodInterceptor>(),
                    new InterceptionBehavior<PolicyInjectionBehavior>()
                    );

            return container.Resolve<T>();
        }

        #region IActionHandler
        public string HandlerName()
        {
            return this.GetType().Name;
        }

        public umbraco.interfaces.IAction[] ReturnActions()
        {
            return new umbraco.interfaces.IAction[] { _proxy.Action };
        }

        public bool Execute(umbraco.cms.businesslogic.web.Document documentObject, umbraco.interfaces.IAction action)
        {
            return _proxy.Invoke(documentObject);
        } 
        #endregion
        
    }
}

La classe ActionInterceptorBase sera utilisée comme base pour chacun de nos action handlers. Plutôt que d’implémenter IActionHandler comme nous devrions habituellement le faire pour réagir à une action du back-office, nous implémenterons cette classe. Nous pourrons grâce à elle injecter un pipeline de « réactions » (plutôt qu’une liste de « réactions » sans ordre défini). Par ailleurs, l’ajout d’une « réaction » se fera à travers la section de configuration policyInjection.
La méthode GetInterceptor permet d’obtenir une instance — interceptée — de notre proxy ActionProxy<T>.

Enfin, il reste à créer une implémentation de ActionInterceptorBase pour chaque type d’action que nous souhaitons pouvoir intercepter. Voici quelques exemples:

using System;

namespace Umbraco.Interception.Actions
{
    public class ActionPublishInterceptor : ActionInterceptorBase
    {
        public ActionPublishInterceptor()
            : base(GetInterceptor<ActionProxy<umbraco.BusinessLogic.Actions.ActionPublish>>())
        {

        }
    }

    public class ActionDeleteInterceptor : ActionInterceptorBase
    {
        public ActionDeleteInterceptor()
            : base(GetInterceptor<ActionProxy<umbraco.BusinessLogic.Actions.ActionDelete>>())
        {

        }
    }

    public class ActionMoveInterceptor : ActionInterceptorBase
    {
        public ActionMoveInterceptor()
            : base(GetInterceptor<ActionProxy<umbraco.BusinessLogic.Actions.ActionMove>>())
        {

        }
    }

    public class ActionNewInterceptor : ActionInterceptorBase
    {
        public ActionNewInterceptor()
            : base(GetInterceptor<ActionProxy<umbraco.BusinessLogic.Actions.ActionNew>>())
        {

        }
    }

    // [...]
}

Finalement, il faut implémenter une matching rule par action interceptable. Rappelons que la matching rule sert à identifier chaque méthode à intercepter pour la mise en place de l’interception à partir du fichier de configuration. Voici quelques exemples:

using System;
using System.Collections.Specialized;

using Microsoft.Practices.EnterpriseLibrary.PolicyInjection.Configuration; // pour CustomCallHandlerData
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration; // pour ConfigurationElementTypeAttribute
using Microsoft.Practices.Unity.InterceptionExtension; // pour IMatchingRule

namespace Umbraco.Interception.MatchingRule.Action
{
    [ConfigurationElementType(typeof(CustomMatchingRuleData))]
    public class ActionPublishMatchingRule : IMatchingRule
    {
        private static readonly Type TARGET_TYPE = typeof(umbraco.BusinessLogic.Actions.ActionPublish);

        public bool Matches(System.Reflection.MethodBase member)
        {
            Type type = member.ReflectedType;

            return (
                member.Name.Equals(ActionInterceptors.ActionProxyBase.TARGET_METHOD)
                && type.IsGenericType
                && type.GetGenericArguments()[0].Equals(TARGET_TYPE)
                );
        }

        public ActionPublishMatchingRule(NameValueCollection attributes)
        {

        }
    }

    [ConfigurationElementType(typeof(CustomMatchingRuleData))]
    public class ActionDeleteMatchingRule : IMatchingRule
    {
        private static readonly Type TARGET_TYPE = typeof(umbraco.BusinessLogic.Actions.ActionDelete);

        public bool Matches(System.Reflection.MethodBase member)
        {
            Type type = member.ReflectedType;

            return (
                member.Name.Equals(ActionInterceptors.ActionProxyBase.TARGET_METHOD)
                && type.IsGenericType
                && type.GetGenericArguments()[0].Equals(TARGET_TYPE)
                );
        }

        public ActionDeleteMatchingRule(NameValueCollection attributes)
        {

        }
    }

    // [...]
}

Et voilà.

La mise en place du pipeline (via le fichier de configuration) est décrite à la fin de la première partie de cette courte série.

Toute cette tuyauterie (simple mais effectivement conséquente en terme de nombre de classes) permet d’intercepter tout événement ou action effectuée dans le back-office d’Umbraco et d’y réagir avec une suite de méthodes, dans un ordre donné, et indépendantes entre-elles.