In many cases, using a caching mechanism can drastically improve performances. Processed results of methods can be stored in memory or in files so they are ready to be returned immediately when we call the methods again.
In some .NET applications, we can use the MemoryCache class from System.Runtime.Caching to store data in memory. However, using such library in a basic way may induce some problems such as:
- Adding caching in each method which needs it may result in less readable and polluted code.
- The repetitive use of such code may induce errors (especially copy-pasted code).
- Depending on the nature of projects (mobile apps, websites, web services, desktop apps), we may need a different caching approach.
In order to avoid all these problems, I have written a generic caching aspect using Autofac and Castle DynamicProxy.
Demo
Caching the result of a method is as simple as the following code:
Here, we call the method many times but before the last call we do a 2 seconds break:
As expected, the three first calls returned the same Guid instead of three different because the first call result were cached for one second. After the two seconds break, another result were processed for the last call as the first value has expired.
Implementing the caching aspect
First, we have to create a caching abstraction using an interface. Depending on the project’s nature, the implementation will be different. In my case, I will use the MemoryCache default singleton which is good for desktop applications:
Secondly, we have to create the CacheResult attribute:
In a third time, we need to implement the class that will intercept the methods decorated with the CacheResult attribute. For each method call, we will compute a unique key based on the class name, the method name and the parameters. If the result exists in the cache provider, we will return it as is. Otherwise, the method will be executed and the result will be stored in the cache with the specified duration in milliseconds:
Finally, we need to wire all these classes with an Autofac module:
Thanks for the code snippet.
One remark: the code in CacheResultInterceptor line 24 assumes that ToString() always produces different results for different parameters, which might not be the case for non-primitive types (actual objects). Thus, CacheResultInterceptor may encounter a false cache hit and return a wrong result. Also, for some tricky combinations of string parameters containing “-” the concatenation logic may fail.
I implemented a version that computes a hashcode over all parameters, performs the lookup using the method signature and this hashcode, and then again compares each parameter using Equals(). This requires a lookup of lists.
Jonathan can you please upload your enhanced demo, or clarify what you have done at the following:
CacheResultInterceptor may encounter a false cache hit and return a wrong result. Also, for some tricky combinations of string parameters containing “-” the concatenation logic may fail.
I implemented a version that computes a hashcode over all parameters, performs the lookup using the method signature and this hashcode, and then again compares each parameter using Equals()
Some concept to cater for complex type argument…
private string GenerateCacheKey(IInvocation invocation)
{
byte[] h = hasher.ComputeHash(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(invocation.Arguments)));
StringBuilder sb = new StringBuilder();
sb.Append(invocation.TargetType.FullName);
sb.Append(“.”);
sb.Append(invocation.Method.Name);
sb.Append(“#”);
for (int i = 0; i < h.Length; i++)
{
sb.Append(h[i].ToString("x2"));
}
return sb.ToString();
}