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.

Published 31/5/2008 23:27 por Rodrigo Corral

Comentarios

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

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!!

Monday, June 02, 2008 10:14 AM by Ibon Landa

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

Voy a hacer de malo...

¿Queremos cachear a nivel de servicios web? ¿No es algo que corresponde a la capa de negocio?

:-)

David.

Monday, June 02, 2008 10:57 AM by David

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

Ay, Señor Corral,

veo con lástima que los talibanes ortográficos no conseguimos hacer mella en su redacción...

Eso o que cuando busca lo hace de modo esdrújulo.    :P

"Búscando"

Le dejo, un enlace que le puede ayudar:

es.wikipedia.org/.../Acentuaci%C3%B3n_del_idioma_espa%C3%B1ol

Un saludo cordial y talibán.

J

Monday, June 02, 2008 12:59 PM by J

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

Confunde el talibán

guiado por su mal

un error ortográfico

con uno tipográfico.

Saludos.

Monday, June 02, 2008 1:40 PM by Rodrigo Corral

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

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.

Monday, June 02, 2008 1:47 PM by Rodrigo Corral

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

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!

Monday, June 02, 2008 4:18 PM by Augusto Ruiz

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

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!

Monday, June 02, 2008 9:11 PM by Rodrigo Corral

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

Muy interesante el post, es más me lo anoto para una próxima charlita.

Saludos

Jualu

Sunday, June 15, 2008 10:25 PM by Juan Luis Guerrero Minero

Deja tu comentario

(requerido) 
(requerido) 
(opcional)
(requerido)