Tuesday, August 23, 2011

cache.Get(()=>GetLongRunningDataCall(…)) - Cache service based on lambda expressions

On current project I’m working with a web farm and long running backend web services. So we have to cache all web-service calls in our distributed cache. Distributed cache can store only serializable objects of course.

Lets say we have long running method “GetCustomerInformation(string customerId, Language language)” which returns some huge object graph based on input parameters.

With current implementation we have to wrap all operation calls by this template:


CustomerInfo GetCustomerInfoCached(string customerId, Language language)
{
var res = TryGetFromCache<CustomerInfo>(out wasInCache, “GetCustomerInformation”, customerId, language);
if(wasInCache) return res;
res= GetCustomerInformation(customerId,language); //long running call
StoreInCache<CustomerInfo>(res, “GetCustomerInformation”, customerId, language);
return res;
}




Let’s say I would like to replace all this code with something like this:



var res = cache.Get(()=>GetCustomerInfo(customerId,language));


Where this cache.Get(…) will do internally all this crap you see in the first code block. Can I do this? Like Obama says “Yes I can!”. And here is the way to do this, which use GetHashCode extension:




using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Collections;

namespace PMunin.com
{
/// <summary>
/// Cache service that supports Lambda expression
/// </summary>
public class Cache
{

/// <summary>
/// Fake operation required to make GetParameterKey working
/// </summary>
/// <param name="pars"></param>
/// <returns></returns>
static object[] GetParameterValues(params object[] pars)
{
return pars;
}
/// <summary>
/// Get instantiated parameter values of specified method call expression.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parametrizedMethodCall">Method call Lambda expression. Like "()=>GetSomeLongExecutedFunction(par1,par2,"par3",...)"</param>
/// <returns></returns>
protected object[] GetParameterValuesByExpression<T>(Expression<Func<T>> parametrizedMethodCall)
{
// We need to create lambda expression based on specified method call expression
// But replace in it MethodCall with another method retrieving parameter values, keeping the same arguments

var args = ((MethodCallExpression)parametrizedMethodCall.Body).Arguments.Select(arg => Expression.Convert(arg, typeof(object))).ToArray();
// All arguments must be boxed for GetParameterValues operation, otherwise runtime exception

Func<object[], object[]> getParamValuesDelegate = GetParameterValues;
var getMethodCallParameterValuesExpression = Expression.Lambda<Func<object[]>>(
Expression.Call(getParamValuesDelegate.Method, Expression.NewArrayInit(typeof(object), args))
);
//Create expression replacing method call with a GetParameterValues

var getMethodCallParameterValues = getMethodCallParameterValuesExpression.Compile();
return getMethodCallParameterValues();
}

/// <summary>
/// Calculates uniq key for particular method-call lambda expressions
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parametrizedMethodCall"></param>
/// <returns></returns>
protected virtual string GetFunctionCacheKey<T>(Expression<Func<T>> parametrizedMethodCall)
{
var parameterValues = GetParameterValuesByExpression(parametrizedMethodCall);
var hc = parameterValues.GetHashCodeOfSerializableContent();
var fMethod = ((MethodCallExpression)parametrizedMethodCall.Body);
var fullMethodName = fMethod.Method.DeclaringType.FullName + "." + fMethod.Method.Name;
var cacheKey = fullMethodName + hc.ToString();
return cacheKey;
}

/// <summary>
/// Removes cache of this particular operation
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parametrizedMethodCall">Method call Lambda expression. Like "()=>GetSomeLongExecutedFunction(par1,par2,"par3",...)"</param>
public void Invalidate<T>(Expression<Func<T>> parametrizedMethodCall)
{
RemoveFromCache(GetFunctionCacheKey(parametrizedMethodCall));
}

/// <summary>
/// Try to get function result from Cache. If it was not cached yet, execute function, caches result and return it.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parametrizedMethodCall">Method call Lambda expression. Like "()=>GetSomeLongExecutedFunction(par1,par2,"par3",...)"</param>
/// <returns></returns>
public T Get<T>(Expression<Func<T>> parametrizedMethodCall)
{

var cacheKey = GetFunctionCacheKey(parametrizedMethodCall);
object result = null;
if (TryGetFromCache(cacheKey, out result))
return (T)result;

var func = parametrizedMethodCall.Compile();
result = func();
StoreToCache(cacheKey,result);

return (T)result;
}


/// <summary>
/// Default cache storage
/// </summary>
Dictionary<string, object> cacheDictionary = new Dictionary<string, object>();
/// <summary>
/// Retrieves information from CacheStorage
/// </summary>
/// <param name="cacheKey"></param>
/// <param name="result"></param>
/// <returns></returns>
protected virtual bool TryGetFromCache(string cacheKey, out object result)
{
return cacheDictionary.TryGetValue(cacheKey,out result);
}
/// <summary>
/// Store object to Cache Storage
/// </summary>
/// <param name="cacheKey"></param>
/// <param name="objectToCache"></param>
protected virtual void StoreToCache(string cacheKey, object objectToCache)
{
cacheDictionary[cacheKey] = objectToCache;
}
/// <summary>
/// Removes record from cache storage for particular key
/// </summary>
/// <param name="cacheKey"></param>
protected virtual void RemoveFromCache(string cacheKey)
{
cacheDictionary.Remove(cacheKey);
}

}
}



 





 


And here is the example how it’s gonna be used in “consumer’s” code:




using System;
using System.Linq;
using System.Threading;

namespace PMunin.com
{

class Program
{


/// <summary>
/// Some long running operation (like getting things from Database or WebServices)
/// </summary>
/// <param name="ctr"></param>
/// <returns></returns>
static string LongRunningDataCall(MyContainerObject ctr, int i)
{
Thread.Sleep(1000);
//returns first item.Str, or "empty"
return (ctr.ItemsInContainer.FirstOrDefault() ?? new MyItemInContainer() { Str="empty"}).Str;
}


static void Main(string[] args)
{
Cache cache = new Cache();

MyItemInContainer subObject;
var ctr = new MyContainerObject()
{
ItemsInContainer = new []
{
(subObject = new MyItemInContainer(){Str="Item1"})
}
};

//This is first execution with this combination of input parameters. LongRunningDataCall will be executed
var res = cache.Get(() => LongRunningDataCall(ctr, 1));
//now cache has ({Str"Item1",Int=0}, 1)

//This inp. parameters combination is already in cache. LongRunningDataCall is not executed here
var res2 = cache.Get(() => LongRunningDataCall(ctr, 1));

subObject.Int = 25; //Changing sub the object of

//Argument changed - will execute BackendLongOperation
res = cache.Get(() => LongRunningDataCall(ctr, 1));
//now cache has
// ({Str"Item1",Int=0}, 1)
// ({Str"Item1",Int=25}, 1)


//Complicated lambda expression works as well!
res = cache.Get(() => LongRunningDataCall(
new MyContainerObject()
{
ItemsInContainer = new MyItemInContainer[]
{
new MyItemInContainer(){Str=myContextVariable} //myContextVariable=="Item1", which is already in cache
}
},1
)); //This input parameter is already in cache will take it from there, without executing target operation

}

static string myContextVariable = "Item1";
}


[Serializable]
public class MyContainerObject
{
public MyItemInContainer[] ItemsInContainer { get; set; }
}

[Serializable]
public class MyItemInContainer
{
public string Str { get; set; }
public int Int { get; set; }
}
}






Possible issues and limitations:


- I assume using reflection and LambdaExpression.Compile() may meet some security limitations in not FullTrust enviroments. However I didn’t verify it out.


- Objects for input parameters must be serializable to estimate hash code properly


 


Please let me know if you see any other issues.