Click here to Skip to main content
15,896,557 members
Articles / Programming Languages / C#
Tip/Trick

Simple Caching with AOP

Rate me:
Please Sign up or sign in to vote.
4.80/5 (4 votes)
30 Apr 2014CPOL2 min read 9.7K   68   6  
An implementation of a Simple Cache using PostSharp, ConcurrentDictionary with a Lazy value and dynamics

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.

C#
[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.

C#
public override void RuntimeInitialize(MethodBase method)
{
   base.RuntimeInitialize(method);

   var declaringType = method.DeclaringType;

   // Use HashCodes to shorten the cache key.
   _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:

C#
public override void OnInvoke(MethodInterceptionArgs args)
{
   // Build the cache key combining the method key with calling arguments key
   var key = string.Format(CultureInfo.CurrentCulture, KeyFormat, this._methodKey, BuildKey(args));

   // Interrogate the cache, returning an existing result, if found.
   // For a new key, add a Lazy value wrapping the call to the method and return value.
   dynamic result = Cache.GetOrAdd(
       key,
       x => new Lazy<object>(() =>
        {
            args.Proceed();
            return args.ReturnValue;
        })).Value;

    // Overlay the method result with the cache result.
    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.

C#
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:

Image 1

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:

  1. How useful dynamic can be when dealing with unknowns.
  2. How simple the class is. I was expecting a more complex solution.

History

  • Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --