Caching Data Operations Using Delegates

by Captain Database on Sep 19th in C#

Not too long ago, I had a situation where I wanted to enforce a caching policy on an SOA data model. While this is a pretty trivial alteration, it’s one of those situations where the code will only be as easy to maintain as it is generic. Luckily, insane levels of abstraction (among other things) are a specialty of mine.

Using an interface that defines all of the data operations, we can utilize delegates to streamline the caching process while still maintaining our façade pattern. That way, our caching mechanism can serve as a proxy, instead of just adding additional complexity.

First things first, we want to add our proxy method. Essentially, we’ll be able to call this method and pass the delegate that we want it to call for us. Instead of simply executing the method, we first need to check for it in the cache. After the value has been retrieved, we need to determine the TTL for the cache item and insert it (this part is using a custom CacheableAttribute class for annotation). So, here we go:

/// <summary>
/// Gets the data for the specified method call from the appropriate source. If the
/// data is already present in cache, the method will not be invoked. If the data is
/// not cached, but cacheable, the return value from the data layer provider will be
/// cached.
/// </summary>
/// <typeparam name="T">The return type of the data method.</typeparam>
/// <param name="oDataLayerFunc">The IDataLayer method from which data will be retrieved.
/// This method can be specified by a function pointer, delegate, or lambda expression.</param>
/// <returns>The value of the data method.</returns>
public T Get<T>(Func<IDataLayer, T> oDataLayerFunc)
{
	//Get the optimized cache key from the function below
	string sKey = GetCacheKey(oDataLayerFunc);

	//Determine if the data is cacheable based on whether or not the type has the Cacheable
	//attribute applied to it
	TimeSpan oLifetime = CacheableAttribute.GetLifetime(typeof(T));

	//If the data is cacheable, use the delegate's key to search cache for it
	if (oLifetime != CacheLifetime.None)
	{
		object oCacheValue = Cache[sKey];

		//If the value was found in cache, don't execute the data layer method
		if (oCacheValue != null && oCacheValue is T)
			return (T)oCacheValue;
	}

	//If the value couldn't be found in cache, invoke the data layer method
	T oDataValue = oDataLayerFunc.Invoke(DataLayer);

	//We also now want to look for a more specific cacheable attribute
	oLifetime = CacheableAttribute.GetLifetime(oDataValue);

	//If the data is cacheable, store the new value
	if (oLifetime != CacheLifetime.None)
	{
		Cache.Insert(sKey, oDataValue, null,
			DateTime.Now.Add(oLifetime), Cache.NoSlidingExpiration);
	}

	//Return the result
	return oDataValue;
}

I know, it’s kind of crazy. But wait, there’s more! That GetCacheKey method I referenced, how are we actually going to implement that? We need to cache the value by what method is being called and what arguments are being passed to it. And still, we want it to be as automatic as possible, so that we don’t create a maintenance nightmare down the line.

The solution requires a bit of reflection, but it doesn’t go too deep:

/// <summary>
/// Gets the cache key that should be used to identify the specified delegate.
/// </summary>
/// <param name="oFunction">The delegate function to identify.</param>
/// <returns>A string containing a unique identifier for the delegate and its
/// arguments.</returns>
public static string GetCacheKey(Delegate oFunction)
{
	//To make this easier to visualize, there are several comments along the way
	//referring to what the current key is when the StringBuilder is updated.
	//These all assume that the delegate is a call to IDataLayer.GetRegions(4)

	//The delegate will always be a member of IDataLayer, so all we need to
	//identify the method is its reflected name
	StringBuilder sbKey = new StringBuilder();
	sbKey.Append(oFunction.Method.Name);
	//Current key: <GetRegions>b__8

	//If we can identify information in the target object, these are essentially
	//query parameters/arguments -- they should be part of the cache key
	if (oFunction.Target != null)
	{
		//Get metadata about the target object and its fields
		object oTarget = oFunction.Target;
		Type tTargetType = oTarget.GetType();
		FieldInfo[] aMembers = tTargetType.GetFields();

		//All of the arguments must be added to the key
		//i.e. GetRegions(4) should have a different cache key than GetRegions(1)
		foreach (FieldInfo f in aMembers)
		{
			//Add a delimiter between arguments
			sbKey.Append(';');
			//Current key: <GetRegions>b__8;

			object oValue = f.GetValue(oTarget);
			//If the argument has a value, add it with delimiter characters escaped
			if (oValue != null)
			{
				sbKey.Append(oValue.ToString().Replace(";", @"\;"));
				//Current key: <GetRegions>b__8;4
			}
		}
	}

	//The compiled string is a unique method call identifier
	return sbKey.ToString();
	//Final key: <GetRegions>b__8;4
}

I know, you probably feel like you just watched Inception for the first time. But if you examine/test it until you figure out how it all works, I think you’ll see the elegance of it (or at least the reason why I have insomnia).

Leave a Reply

Powered By Wordpress Designed By Ridgey