Introduction
This article represents an easy method for implementing caching in C# using the power of PostSharp. Two problems that might arise from the use of caching are also discussed and solutions will be suggested.
In this article, I will be a using a simple example which is a repository of books and I will try to implement caching on a method called GetBooks()
, which retrieves books by author. Obviously, for the purpose of this article, this is a contrived example that might not need caching, because if the database is too small, then caching is not needed, and if it is too big, then it will most probably be impractical to cache this huge amount of data. However, it is sufficient to demonstrate the point of this article.
Using .NET Framework's MemoryCache to Implement Caching
The GetBooks()
method which I would like to cache is implemented like this:
public static IEnumerable<Book> GetBooks(string authorName)
{
Thread.Sleep(1000);
using (var repository = new BookRepository())
{
return repository.Authors
.Where(a => a.Name == authorName)
.SelectMany(a => a.Books)
.ToList();
}
}
Notice that I added a sleep of 1000 milliseconds to simulate a slow method. The goal now is to support caching in this method so that we can return the result immediately if it is cached. We will employ the MemoryCache
object of .NET Framework and modify our function to look like the following:
public static IEnumerable<Book> GetBooks(string authorName)
{
var result = MemoryCache.Default.Get(authorName) as IEnumerable<Book>;
if (result != null)
return result;
Thread.Sleep(1000);
using (var repository = new BookRepository())
{
result = repository.Authors
.Where(a => a.Name == authorName)
.SelectMany(a => a.Books)
.ToList();
}
MemoryCache.Default.Set(authorName, result,
DateTimeOffset.Now.Add(new TimeSpan(0, 0, 30, 0)));
return result;
}
Now that we have enhanced our method to support caching, let's run the method 10 times to see how it performs. I am not going to waste space here by pasting the test code or the result, but briefly, it took around 1083.0619 secs to execute the method for the first time, and -as you might have already guessed- 0 milliseconds to execute the method for the other nine times. Yes, our caching is working! Sadly, there are some concerns and problems here!
The Evil of Duplication
The main problem with the code above is duplication, or the evil of duplication as Andrew Hunt and Dvaid Thomas like to call it in The Pragmatic Programmer. For if we want to implement caching in some 100 methods, we will need to copy, paste, and adjust the code 100 times. And what if after implementing caching in 100 methods, we decide that we want to make some modification to the caching mechanism? Or what if we discover a bug that we never thought of? This is where Aspect Oriented Programming comes in handy. Sadly, and really sadly, .NET Framework doesn't support AOP out of the box (I actually still don't know why Microsoft doesn't implement AOP in .NET; in Python, for example, AOP is extremely easy with the use of decorators.) However, using a 3rd-party tool called PostSharp, we can implement AOP in C# (and I assume VB.NET).
Among the many features it has, PostSharp allows you to intercept the behaviour of a method by using the attribute MethodInterceptionAspect
, after inheriting from it of course to insert your interception. The following example illustrates the idea:
[Serializable]
class MyInterceptionAttribute : MethodInterceptionAspect
{
public override void OnInvoke(MethodInterceptionArgs args)
{
base.OnInvoke(args);
}
}
As you can see, the idea is simple: override OnInvoke()
, do some pre-processing, call the base OnInvoke()
, and then do some post-processing. As you might have already guessed, you don't have to call base.OnInvoke()
so long as you manually set args.ReturnValue
if you want to return a value. So to implement caching, all we need to do is check if we have the result cached, and if it is, manually set args.ReturnValue
and then return from the function without calling base.OnInvoke()
. If we don't have the result in the cache, we will call base.OnInvoke()
and then cache args.ReturnValue
.
Before such an implementation can return the correct result, we need to use different keys in the cache for different arguments passed to the method, otherwise the method will always return the same value after the first call regardless of the input parameters. PostSharp exposes the arguments the method is being called in args.Arguments
, so we will be using this to generate a unique key for the result being cached:
[Serializable]
class CacheableResultAttribute : MethodInterceptionAspect
{
public override void OnInvoke(MethodInterceptionArgs args)
{
var cache = MethodResultCache.GetCache(args.Method);
var result = cache.GetCachedResult(args.Arguments);
if (result != null)
{
args.ReturnValue = result;
return;
}
base.OnInvoke(args);
cache.CacheCallResult(args.ReturnValue, args.Arguments);
}
}
As you can see, we are calling the method GetCacheResult()
, the implementation of which is discussed later when we discuss the implementation of MethodResultCache
, and pass the arguments given by PostSharp. Later in the method, we call CacheCallResult()
and -again- pass the arguments given by PostSharp along with the result so that we cache the result for the next run. Also, we are retrieving the cache object by passing args.Method
to MethodResultCache.GetCache()
, which will return a different cache object for different method names. This way, we avoid overwriting the cached result by another method if it is called with the same arguments. It also has another advantage which we will discuss later, so please be patient.
Now that we have implemented an attribute that intercepts the behaviour of a method to support caching, all we need to do is add this attribute to any method that we want to support caching in. Applying this attribute on our GetBooks()
method, it looks like the following now:
[CacheableResult]
public static IEnumerable<Book> GetBooks(string authorName)
{
Thread.Sleep(1000);
using (var repository = new BookRepository())
{
return repository.Authors
.Where(a => a.Name == authorName)
.SelectMany(a => a.Books)
.ToList();
}
}
Pretty concise and lovely, isn't it?
Invalidating Cached Results
While the code above implements caching in a very neat way, it suffers from a potential major problem. What happens if our data changes and the cached result is not correct anymore? If you are ready to accept that changes take some time to get reflected, then you don't need to worry about this problem, otherwise, you will need to invalidate the cached results to ensure that the next time your method is called, the data will be fetched again from the database.
Solving this problem is not as you easy as it might seem, for how do we tell which cached results we need to invalidate? For example, if we modify the books authored by Ross L. Finney, we should ideally avoid invalidating the cached results of the books authored by Howard Anton. I believe solving this problem is context-dependent, i.e., there is no generic solution, and is out of the scope of this article, so I will implement a simple solution, which is to invalidate all the cached results when a change happens, but limit it to only the methods that might be affected. For example, if the ISBN of a book changes (not sure whether this is actually possible, but assuming so for the sake of this example), then we need to invalidate the results of the method GetBookAuthorByIsbn()
, but not the results of the method GetBookAuthorByBookName()
. To make this possible, and to avoid having to loop through the cached results and check their keys, which might be a very slow operation, I am using a different cached object for each method.
For example, let's implement a method that allows us to change an author name:
public static void ChangeAuthorName(string oldName, string newName)
{
using (var repository = new BookRepository())
{
repository.Authors.Single(a => a.Name == oldName).Name = newName;
repository.SaveChanges();
MethodResultCache.GetCache(typeof (BookApi).GetMethod("GetBooks")).ClearCachedResults();
}
}
As you can see, I am retrieving the cache for the method GetBooks()
and then clearing it. We can simplify this code a little bit by implementing another interception attribute that automatically retrieves the cache and calls ClearCachedResults()
; sadly, though, we still need to pass a list of all the methods that are going to be affected by a call to this method:
[AffectedCacheableMethodsAttribute("EasyCaching.APIs.BookApi.GetBooks")]
public static void ChangeAuthorName(string oldName, string newName)
{
using (var repository = new BookRepository())
{
repository.Authors.Single(a => a.Name == oldName).Name = newName;
repository.SaveChanges();
}
}
Authentication and Authorization Consideration with Caching
Another problem that arises when using the CacheableResultAttribute
above is related to authentication and authorization, or in fact the possibility of breaking the security of the APIs by using this attribute. To explain this, imagine this scenario. User A has permission to call GetBooks()
, but user B doesn't. We thus change the GetBooks()
method to check the logged-in user and verify that he/she has permission to call this method, if not throw, for example, an AccessDeniedException
. Now if user A calls the GetBooks()
method, the security check will pass and the result will be retrieved and cached. If user B then calls the GetBooks()
method, CacheableResultAttribute
will intercept the call and return the cached result because it already has it. Thus, we broke the security of the method!
To solve this problem, we can simply update our attribute to use the currently logged-in user as an additional argument:
[Serializable]
class CacheableResultAttribute : MethodInterceptionAspect
{
public override void OnInvoke(MethodInterceptionArgs args)
{
var cache = MethodResultCache.GetCache(args.Method);
var arguments = args.Arguments.Union(new[] {WindowsIdentity.GetCurrent().Name}).ToList();
var result = cache.GetCachedResult(arguments);
if (result != null)
{
args.ReturnValue = result;
return;
}
base.OnInvoke(args);
cache.CacheCallResult(args.ReturnValue, arguments);
}
}
This will append a new argument which is the user name to the key of the method call. For example, calling GetBooks("Ross L. Finney")
will generate the following key "EasyCaching.APIs.BookApi.GetBooks(Ross L. Finney, <user name>)
" instead of "EasyCaching.APIs.BookApi.GetBooks(Ross L. Finney)
". This way, user A will have a different cache key than user B, and our problem is solved!
This solution doesn't only solve security problems, but also the problem when a certain method might return different results based on the logged in user, for example GetMyBooks()
.
The MethodResultCache Class
For those interested in knowing how the MethodResultCache
class is implemented, yet too lazy to download the source code attached with this article, here is how it is implemented:
class MethodResultCache
{
private readonly string _methodName;
private MemoryCache _cache;
private readonly TimeSpan _expirationPeriod;
private static readonly Dictionary<string, MethodResultCache> MethodCaches =
new Dictionary<string, MethodResultCache>();
public MethodResultCache(string methodName, int expirationPeriod = 30)
{
_methodName = methodName;
_expirationPeriod = new TimeSpan(0, 0, expirationPeriod, 0);
_cache = new MemoryCache(methodName);
}
private string GetCacheKey(IEnumerable<object> arguments)
{
var key = string.Format(
"{0}({1})",
_methodName,
string.Join(", ", arguments.Select(x => x != null ? x.ToString() : "<Null>")));
return key;
}
public void CacheCallResult(object result, IEnumerable<object> arguments)
{
_cache.Set(GetCacheKey(arguments), result, DateTimeOffset.Now.Add(_expirationPeriod));
}
public object GetCachedResult(IEnumerable<object> arguments)
{
return _cache.Get(GetCacheKey(arguments));
}
public void ClearCachedResults()
{
_cache.Dispose();
_cache = new MemoryCache(_methodName);
}
public static MethodResultCache GetCache(string methodName)
{
if (MethodCaches.ContainsKey(methodName))
return MethodCaches[methodName];
var cache = new MethodResultCache(methodName);
MethodCaches.Add(methodName, cache);
return cache;
}
public static MethodResultCache GetCache(MemberInfo methodInfo)
{
var methodName = string.Format("{0}.{1}.{2}",
methodInfo.ReflectedType.Namespace,
methodInfo.ReflectedType.Name,
methodInfo.Name);
return GetCache(methodName);
}
}
Things to note in this code:
- The method
GetCacheKey()
accepts an enumeration of arguments and converts each to string
using ToString()
. Thus, you need to ensure that the ToString()
implementation of all arguments generate distinct string
s for distinct values, otherwise the cache key for different calls of the same method might generate the same key. - The method
ClearCachedResults()
disposes the cache object and creates a new one. The reason is that I didn't find a method to clear the cached values. - The function
GetCache(MemberInfo methodInfo)
uses the namespace and class name while generating the method name to avoid having two methods with the same name from different classes overwriting the cached values of each other.
Conclusion
As you can see, using PostSharp greatly simplifies changing the behaviour of methods in a generic and reusable way. We used it here to support caching, but it is not limited to this, and the ideas are plenty. For example, you can implement an attribute that allows you to log the execution time of a method and thus continuously profile your methods. So if you haven't used it before, it is time to!