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)].
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.