Bricomanía: añadiendo caché a nuestros servicios WCF

Resulta que andaba yo el otro día, como loco, buscando el equivalente al parámetro CacheDuration del atributo WebMethod de ASP.Net en WCF. Resumiendo necesitaba algo que me permitiese establecer declarativamente, en tiempo de desarrollo, que las llamadas a una operación de un servicio WCF devolviesen un resultado cacheado y que este caducase cada cierto tiempo. Esta posibilidad que nos brinda ASP.Net nos permite mejorar muchísimo la escalabilidad de nuestros servicios evitando que lleguen hasta el almacen de datos peticiones de datos que cambiaban muy raramente o evitando que se ejecuten completamente operaciones que puedan ser costosas.


Después de buscar sin exito durante algún tiempo por la MSDN y algunos otros sitios el equivalente en WCF a la carácteristica que tanto me había ayudado cuando desarrollaba servicios con ASP.Net hice lo que siempre hago cuando la MSDN y Google me fallan, llamar a Unai (es conocido el mito de que Fraga se sabía la guia de teléfonos de Galicia de memoria y dicen también que hay otro que se sabe la MSDN). Unai certificó mis temores, no habia nada equivalente al CacheDuration en WCF.


Pero claro Unai, que en lo que a .Net se refiere es el equivalente al barbas de bricomanía, me dijo: tio, si no lo hay te lo haces, para algo WCF es extensible y flexible y un montón de cosas más…. ¿no?. ¿Alguna pista? le pregunte… y claro me dio la pista adecuada, vete a tu tienda de bricolage favorita y comprate un IOperationBehavior y un IOperationInvoker me dijo… y luego ya sabes la herramientas habituales, Visual Studio, WCF, un poco de AOP, lijar, pintar y pulir…


Pero claro, yo soy más de Ikea que de Leroy Merlin, así que me puse a buscar algo más ‘de montar’ que ‘de hacer’… y con la pista de Unai encontre un motón de implementaciones de lo que yo necesitaba…


Pero ya sabemos lo que pasa con Ikea, o te sobran piezas o te faltan o no encajan. Y en esta ocasión no iba a ser diferente. Las implementaciones de IOperationBehavior de caché para WCF que encontré o bien cascaban, o bien no funcionaban con invocaciones asíncronas, o el código era pésimo, o eran una simple prueba de concepto, o todo a la vez… así que tube que pasarme a la táctica ‘hágalo usted mismo’ y, después de enviarle el código a Unai para que me ‘desatascase’ de un error tonto, conseguí resultados. A continuación os comento un poco los fundamentos y el resutado.


Básicamente se trata de implementar un IOperationBehavior en forma de atributo que podemos aplicar a aquellas operaciones de nuestros servicios WCF que queremos que presenten el comportamiento de caché. He llamado a este behavior UseCache. A continuación os muestro el código necesario para aplicar el behavior en cuestión:



   [UseCache(5)]


   public DateTime GetDateTime()


   {



     return DateTime.Now;


   }


El código del IOperationBehavior no puede ser más simple, simplemente se trata de un atributo con una propiedad que establece durante cuanto tiempo se mantendrá cacheado el resultado. El único método que debemos implementar es ApplyDispatchBehavior para sustituir el Invoker estandar de WCF por mi CachedInvoker, que será la clase encargada de cachear los resultados.



  /// <summary>


  /// This attribute is used for adding cache to and WCF operation


  /// </summary>


  [AttributeUsage(AttributeTargets.Method)]


  sealed public class UseCacheAttribute : Attribute, IOperationBehavior


  {


    /// <summary>


    /// Constructor


    /// </summary>


    /// <param name=»cacheDuration»>


    /// The number of seconds the response should be held in the cache.


    /// </param>


    public UseCacheAttribute(int cacheDuration)


    {


      _cacheDuration = cacheDuration >= 0 ? cacheDuration : 0;


    }


 


    /// <summary>


    /// This method applies our operation invoker to the operation


    /// </summary>


    /// <param name=»operationDescription»>Operation description</param>


    /// <param name=»dispatchOperation»>Operation dispatch</param>


    /// <remarks>


    /// <see cref=»IOperationBehavior.ApplyDispatchBehavior»/>


    /// </remarks>


    public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)


    {


      dispatchOperation.Invoker = new CachedInvoker(dispatchOperation.Invoker, _cacheDuration, dispatchOperation.Name);


    }


  }


Una vez que aplicamos este behavior a las operaciones de nuestro servicios cualquier llamada a esa operación pasará a través de nuestro IOperationInvoker que utiliza la cache de ASP.Net para cachear el resultado de la invocación a la operación. Aprovecho para recordar que desde la versión 2 del Framework de .Net se puede usar la cache de ASP.Net en cualquier tipo de aplicación, WCF incluido, aunque esto no es cierto para 1.x. De esta manera, si el resultado ya existe para el valor de los parámetros pasados, se devuelve desde la caché cortocircuitando la llamada antes de que vaya más allá de la fachada de servicios. A continuación pongo el código más relevante del invoker:



  /// <summary>


  /// Operation invoker that adds caching capabilities


  /// </summary>


  public class CachedInvoker : IOperationInvoker


  {


    …


 


    /// <summary>


    /// Implementation for Invoke


    /// </summary>


    /// <param name=»instance»>Instance</param>


    /// <param name=»inputs»>Inputs</param>


    /// <param name=»outputs»>Outputs</param>


    /// <returns>An array of object</returns>


    /// <remarks>


    /// <see cref=»System.ServiceModel.Dispatcher.IOperationInvoker.Invoke»/>


    /// </remarks>


    public object Invoke(object instance, object[] inputs, out object[] outputs)


    {


      string key = GetCacheKey(_operationName, inputs);


      object result = HttpRuntime.Cache[key];


      if (result == null)


      {


        result = _innerInvoker.Invoke(instance, inputs, out outputs);


        HttpRuntime.Cache.Add(


          key, result, null, DateTime.Now.AddSeconds(_cacheDuration),


          System.Web.Caching.Cache.NoSlidingExpiration,


          CacheItemPriority.Default, null);


      }


      outputs = new object[0];


      return result;


    }




    /// <summary>


    /// Get a key for caching based on operation inputs and name.


    /// </summary>


    /// <param name=»inputs»>Inputs</param>


    /// <param name=»operationName»>Operation name</param>


    /// <returns>The key.</returns>


    private static string GetCacheKey(string operationName, object[] inputs)


    {


      string key = operationName;


      for (int i = 0; i < inputs.Length; i++)


      {


        key =  key + ‘$’ + inputs[i].ToString();


      }


      return key;


    }
    …



  }


Una vez aplicado el behavior UseCache a la operación de nuestro servicio las llamadas a esta se comenzarán a cachear. Podéis ver, en el pantallazo siguiente, el resultado de llamar a un método de un servicio que simplemente devuelve la hora actual cada medio segundo tras haberle aplicado el atributo [UseCache(5)].


LLamadas cacheadas


Podéis descargar el código con sus pruebas unitarias y una aplicación de consola de prueba.


En todo proyecto hay servicios que devuelven datos que cambian cada mucho tiempo. Los conceptos mostrados también serian de aplicación para implementar el mecanismo de caché con invalidación de la caché por dependencias usando CacheDependency o SqlCacheDependency, solo cambiarían las líneas que añaden el resultado de la operación a la caché. Espero que os resulte útil.

9 comentarios sobre “Bricomanía: añadiendo caché a nuestros servicios WCF”

  1. Sinceramente no me gusta nada tener que hacer estas cosas y me parece un poco txapu que no lo tenga de serie…

    No parece tan raro necesitar una cache como para que tengas que ponerte a hacer estas cosas, más cuando era una característica que ya existía con servicios web.

    Eso no quita que el código que has puesto me vendrá bien!!

  2. David, interesante apunte… la verdad es que entiendo lo que dices. Al fin y al cabo lo que se puede cachear o no y cuanto tiempo se puede cachear es, en esencia, una decisión de negocio.

    Pero también es cierto que dejar que esa decisión la tome la capa de negocio supone que la petición ya ha supuesto la ejecución de más código del que sería necesario para poderla responder… a lo mejor hay cierta ganancia en cachear ‘lo más temprano posible’.

    Supongo que una vez más, no hay una solución universal. Unas veces querremos cachear a nivel de fachada de servicio y otras a nivel de lógica de negocio.

  3. A mí me preocupa la implementación de GetCacheKey… Sería conveniente meter un separador entre los diversos valores del array inputs, para evitar falsos positivos.

    Además, estaría bien que se recorriese el array y se usase el valor apropiado, en vez de usar siempre el primer elemento del array de inputs. 😉

    Por lo demás, muy interesante, y se expone de forma sencilla cómo personalizar parte del pipeline de WCF 🙂

    Saludos!

  4. Gracias Augusto!!!
    Joder… la verdad es que se me escapo ese bug. La sugerencia del separador cojonuda también, no garantiza la ausencia de falsos positivos pero si que minimiza las posibilidades.

    Lo corrijo ASAP.

    Un saludo!

  5. Estoy intentando implementar un mecanismo de lo que sería una caché «caliente» para una capa de servicios WCF; de tal forma que determinados datos ya estén en caché cuando los clientes de los servicios envíen sus peticiones, evitando la penalización que supone cargar la caché con la primera llamada a la fachada WCF.

    Buscando aquí y allá en la red he encontrado varias opciones como incluir la carga en el directorio App_Code(o similar), la utilización del global.asax, o la utilización de la extensibilidad de WCF a través del atributo Factory.

    Luego de valorar un poco el tema parece que el atributo Factory pueda ser el adecuado. Las dudas que surgen son entorno a pruebas de concepto. En mi caso, necesito que la caché se cargue nada más se levante el pool de aplicaciones del IIS, cuando se levante el servicio básicamente, para que cuando se realicen las llamadas la caché esté cargada prácticamente en su totalidad(nunca estará el 100% porque es también incremental), que al fin y al cabo es un tiempo de penalización que no deben soportar los clientes que llaman a la fachada de servicios WCF. Lo cual a su vez redunda en una gran mejora del rendimiento.

    Temas de como configurar la caché, como invalidarla, o que herramienta utilizar, actualmente me resultan secundarios, ya que he venido trabajando con el bloque de caché de Enterprise Library que para el caso es bastante representativo. Aunque, si es cierto que también estoy sopesando si utilizar AppFabric y su componente de caché son lo suficientemente robustos y flexibles, para un entorno de servicios WCF, donde el rendimiento es el objetivo principal, y como secundario la escalabilidad en un entorno distribuido; pero, la verdad es que si se compara con NetCaché o asimilables, sólo consigo aumentar el número de dudas acerca de la robustez y flexibilidad del componente de caché de AppFabric. Por lo cual de momento, he mantenido una actitud conservadora, antes de aventurarme a subir la solución de producción al banco de pruebas de desarrollo y alojarlo en AppFabric además la consiguiente refactorización de la caché del modulo de infraestructura del proyecto.

    No sé como lo veis vosotros? personalmente, ya os anticipo que lo más apremiante es decidir si la extensibilidad de WCF mediante el atributo Factory cumple mis expectativas para arrancar en un único punto, y una sola vez, cuando se inician los servicios en el IIS.

    Muchas gracias de antemano, cualquier aporte es bienvenido porque a veces pequeños apuntes son invaluables y más que suficientes por el tiempo de decisión o de desarrollo que se gana.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *