Introduction
This simple cache implementation combines a few different techniques into a package that is thread safe and simple to use. It is not designed to handle expiring or updated data, nor is it highly efficient.
Background
The first technique uses PostSharp to support AOP, this code uses the free version of PostSharp. I found AOP in .NET (http://www.manning.com/groves) by Matthew D. Groves to be a very useful introduction.
The second technique uses a ConcurrentDictionary
with a lazy value. The advantage of this is that the underlying method instance only gets called once as the Lazy construct handles multiple requests for the same execution. One place that explains this is ConcurrentDictionary<TKey,TValue> used with Lazy<T>.
The third technique uses a dynamic variable to handle the result. This is handled at runtime and avoids jumping through hoops to handle boxing the results within the aspect code. MSDN: Using Type dynamic (C# Programming Guide) provides some background on dynamics.
Using the Code
This code uses PostSharp that can be added through NuGet's install-package PostSharp. You can use the free version without issue.
The heart of the implementation is in the SimpleCacheAspect
class. As the code will intercept the execution of the method, this class extends MethodInterceptionAspect
. Also, the Cache
is a static readonly
field that is shared across all the methods.
[Serializable]
public class SimpleCacheAspect : MethodInterceptionAspect
{
private static readonly ConcurrentDictionary<string, Lazy<object>> Cache =
new ConcurrentDictionary<string, Lazy<object>>();
Because the Cache is shared, a component of cache key references the method. As the method is unchanging, this can be built once in the RuntimeInitialize
method.
public override void RuntimeInitialize(MethodBase method)
{
base.RuntimeInitialize(method);
var declaringType = method.DeclaringType;
_methodKey = string.Format(
CultureInfo.CurrentCulture,
KeyFormat,
declaringType == null ? 0 : declaringType.GetHashCode(),
method.GetHashCode());
}
With the infrastructure resolved, the method interception is handled with the following:
public override void OnInvoke(MethodInterceptionArgs args)
{
var key = string.Format(CultureInfo.CurrentCulture, KeyFormat, this._methodKey, BuildKey(args));
dynamic result = Cache.GetOrAdd(
key,
x => new Lazy<object>(() =>
{
args.Proceed();
return args.ReturnValue;
})).Value;
args.ReturnValue = result;
}
Once the SimpleCacheAspect
has been added to the solution, you can decorate any method that returns a result with the [SimpleCacheAspect]
annotation. A basic implementation, in the attached source code, demonstrates how the multiple calls are handled with parallel and serial requests.
public class Program
{
public static void Main(string[] args)
{
Parallel.ForEach(
new List<string> { string.Empty, string.Empty, "test" },
(test, pls, index) =>
{
RunMethod(test, index);
RunMethod(test, index);
});
Console.ReadKey();
}
[SimpleCacheAspect]
private static bool TestMethod(string test)
{
Console.WriteLine("TestMethod called for {0}", string.IsNullOrWhiteSpace(test) ? "string.Empty" : test);
using (var mre = new ManualResetEvent(false))
{
mre.WaitOne(1000);
}
return string.IsNullOrWhiteSpace(test);
}
private static void RunMethod(string test, long index)
{
var watch = Stopwatch.StartNew();
Console.WriteLine(
"For index {0} argument {1} the result {2} was returned after {3:N0} ms",
index,
string.IsNullOrWhiteSpace(test) ? "string.Empty" : test,
TestMethod(test),
watch.ElapsedMilliseconds);
}
}
Running the code will output something like this:
This shows that the method was only executed once for each value. The string.Empty
test shows elapsed times of 1,021 ms for both index 0 and index 1. Both entries were waiting for a single execution with the argument of string.Empty
to complete.
Points of Interest
The two biggest surprises after putting this code together were:
- How useful dynamic can be when dealing with unknowns.
- How simple the class is. I was expecting a more complex solution.
History
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.