AOP con Castle Windsor: DynamicInterceptionFacility

Aprovecho el lanzamiento de la Castle Windsor 3.1 RC1, para darle una vuelta a la programación orientada a aspectos usando Castle Windsor.

Hace unos meses hice una pequeña introducción al tema en la que explicaba cómo usar interceptors para aplicar técnicas de AOP con Castle y dejé un ejemplo en github en el que se mostraba cómo realizar la configuración de interceptors usando el Fluent API de Windsor.

Esta vez vamos a ver cómo implementar un método que nos permita cambiar la configuración de interceptors sin necesidad de recompilar la aplicación de una forma cómoda.

Teniendo en cuenta el tipo de cosas que podemos hacer con interceptors, esto resulta útil en bastantes casos, como por ejemplo diagnosticar problemas en producción usando algo parecido a un TraceInterceptor o añadir comportamiento para cubrir algún caso extraño de un cliente con necesidades especiales.

Volviendo a la configuración basada en XML

La mayoría de contenedores soportan varias APIs para realizar su configuración. Casi todos empezaron con un API basada en XML copiada de los contenedores de Java y, con el tiempo, evolucionaron hacia APIs por código que permiten aprovechar la comprobación de tipos del compilador para evitar errores y facilitan el uso convenciones para registro automático de componentes. Podéis ver una buena comparativa de ambas alternativas en este post de Luis Ruiz Pavón sobre la configuración de Autofac.

En general es preferible utilizar la configuración por código porque es mucho más sencilla de mantener, pero en este caso necesitamos poder cambiar la configuración del contenedor sin recompilar la aplicación, por lo que usaremos la configuración XML.

El problema es que en Castle Windsor es incómodo configurar los interceptors desde XML porque no permite aplicar un interceptor a un conjunto de servicios sino que hay que indicarlo servicio a servicio.

Para solventar esto vamos a recurrir a otro de los muchos puntos de extensibilidad que nos ofrece Castle Windsor: las facilities.

DynamicInterceptionFacility

Una facility es una forma de extender el contenedor encapsulando la implementación de nuevas funcionalidades. Por ejemplo, existen facilities para integrar Windsor con WCF, librerías de logging, gestión de eventos, etc.

La facility que vamos a crear nos va a simplificar la tarea de añadir interceptores a clases usando una configuración XML externa.

Mi idea es partir de un fichero de configuración similar a éste:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <facilities>
    <facility type="DynamicInterception.DynamicInterceptorFacility, DynamicInterception">
      <add interceptor="My.Namespace.SampleInterceptor, My.Assembly"
           toTypesMatching="OneConcreteType"/> 
      <add interceptor="My.Namespace.SampleInterceptor, My.Assembly"
           toTypesMatching=".*Controller"/> 
      <add interceptor="My.Namespace.AnotherInterceptor, My.Assembly"
           toTypesMatching="SuperMegaClass"/> 
    </facility>
  </facilities>
</configuration>

En la configuración definimos nuestra facility de la forma estándar y añadimos una serie de nodos add que contienen la siguiente información:

  • interceptor: es el tipo del interceptor que queremos utilizar. Puede estar definido en cualquier assembly, por lo que no hace falta que estuviera implementado en el momento de crear la aplicación, pudiendo crearlo adhoc para cada caso que nos surja.
  • toTypesMatching: es una expresión regular que se usará para saber si el interceptor debe aplicarse a un tipo o no.

Como se ve en el ejemplo, se pueden definir varios interceptores distintos y aplicar un mismo interceptor con varias expresiones regulares distintas añadiendo varios nodos add.

El código para conseguir que todo esto funcione es bastante sencillo:

public class DynamicInterceptorFacility : AbstractFacility
{
    protected override void Init()
    {
        BuildContributors().ToList().ForEach(contributor =>
        {
            if (!Kernel.HasComponent(contributor.InterceptorType))
                Kernel.Register(Component.For(contributor.InterceptorType));

            Kernel.ComponentModelBuilder.AddContributor(contributor);
        });
    }

    private IEnumerable<DynamicInterceptionConstructionContributor> BuildContributors()
    {
        return from child in FacilityConfig.Children
               let typeFilter = new Regex(child.Attributes["toTypesMatching"])
               let interceptorType = Type.GetType(child.Attributes["interceptor"], true)
               where child.Name == "add" 
               select new DynamicInterceptionConstructionContributor(interceptorType, typeFilter);
    }
}

public class DynamicInterceptionConstructionContributor : IContributeComponentModelConstruction
{
    public Type InterceptorType { get; private set; }
    public Regex TypeFilter { get; private set; }

    public DynamicInterceptionConstructionContributor(Type interceptorType, Regex typeFilter)
    {
        InterceptorType = interceptorType;
        TypeFilter = typeFilter;
    }

    public void ProcessModel(IKernel kernel, ComponentModel model)
    {
        if (model.Services.Any(service => TypeFilter.IsMatch(service.FullName ?? "")))
            model.Interceptors.Add(new InterceptorReference(InterceptorType));
    }
}

La forma más sencilla de crear una facility es crear una clase derivada de AbstractFacility, que incluye muchos métodos y propiedades de utilidad para acceder al entorno de la facility. Al añadir la facility al contenedor se invocará el método Init donde podremos interactuar con el Kernel y realizar las configuraciones que necesitemos.

La configuración de la facility está disponible a través de la propiedad FacilityConfig, que expone un interfaz muy similar a la de un documento xml. En nuestro caso, a partir de la configuración estamos generando una serie de objetos DynamicInterceptionConstructionContributor que serán los que realmente lleven el peso de la implementación.

Estos DynamicInterceptionConstructionContributor implementan el interface IContributeComponentModelConstruction, que nos permite modificar la forma en que se construyen los ComponentModel que representan cada servicio registrado en el contenedor. En este caso añadiremos un interceptor del tipo configurado si el nombre del tipo se ajusta a la expresión regular indicada en la configuración.

Una vez que tenemos creados los contributors, la facility se asegura de que el tipo del interceptor está registrado en el Kernel y añade el contributor al Kernel.

Cómo usarlo

Para usar la facility hace falta registrarla en el contenedor, y eso se puede hacer desde código o por XML. Puesto que usar esta facility sólo tiene sentido si vas a cambiar externamente los interceptores, lo lógico es registrarla a través de un fichero XML como el que veíamos antes.

La facility está implementada de forma que no penaliza el rendimiento si no hay interceptores configurados, pero de todas formas lo que suelo hacer con este tipo de cosas es instalarlas mediante un fichero de configuración opcional usando un extension method sobre IWindsorContainer:

public static void InstallOptionalConfigurationFrom(this IWindsorContainer container, string maybeXmlConfigurationFile)
{
    var rootedPath = Path.IsPathRooted(maybeXmlConfigurationFile)
                              ? maybeXmlConfigurationFile
                              : Path.Combine(AppDomain.CurrentDomain.BaseDirectory,     maybeXmlConfigurationFile);
 
    if (File.Exists(rootedPath))
        container.Install(Configuration.FromXmlFile(rootedPath));
}

De esta forma, en el bootstrapper de la aplicación se añade una línea como la siguiente antes de iniciar el registro de componentes:

container.InstallOptionalConfigurationFrom("my.config.xml")

Cuando sea necesario, se puede crear ese fichero y añadir o sobreescribir parte de la configuración realizada por código con configuración externa a través de XML.

Conclusiones

La idea de poder añadir dinámicamente comportamiento a una aplicación sin necesidad de recompilarla es muy atractiva. Al mezclarlo con técnicas de AOP que facilitan la aplicación de cross-cutting concerns se convierte en una herramienta muy poderosa.

Sin embargo, esta idea hay que aplicarla con cuidado porque puede llegar a ser todo demasiado «mágico» dando lugar a problemas difícil de depurar y a un código sobre el que es muy complicado razonar.

El código no deja de ser una prueba de concepto y es mejorable. Los mensajes de error que se van a generar si la configuración es incorrecta son muy poco amigables y no estaría de más poder elegir no sólo las clases que se van a interceptar, sino también los métodos usando el interface IProxyGenerationHook. Si alguien se anima a mejorarlo, el código completo lo puede encontrar en github con su proyecto de test incluido para empezar a trastear con él.

2 comentarios en “AOP con Castle Windsor: DynamicInterceptionFacility

  1. Muy Buen post Juanma, muy bien explicado.
    La verdad es que esto suele ser algo bastante a tener en cuenta si vas a hacer cosas relativas a Cross-cutting y el poder hacerlas «al vuelo», aunque conlleve el riesgo de equivocación al escribir.

    Saludos!

  2. Gracias.

    Yo creo que puede ser útil, siempre y cuando tengas un poco de cuidado. Además hace que depurar la aplicación sea mucho más divertido e interesante cuando no tienes ni idea de qué interceptor está haciendo qué cosas :)

Comentarios cerrados.