.NET Baby Steps: Part VII - Caching
Note: This post is part of a series and you can find the rest of the parts in the series index.
.NET has had one out-of-the-box way to do caching in the past, System.Web.Caching. While a good system, it suffered from two issues. Firstly, it was not extensible, so if you wanted to cache to disk or SQL or anywhere other than memory, you were out of luck. Secondly, it was part of ASP.NET and while you could use it in WinForms, it took a bit of juggling.
The patterns & practices team saw these issues and provided a caching application block in their Enterprise Library which has been used by everyone who did not want to reinvent the wheel. Thankfully, from .NET 4 there is a caching system now included in the framework that solves those two issues. This is known as System.Runtime.Caching.
Slow Example
To see how to use it, let’s start with a process we can cache. I have a class called Demo which has a property named Times of type IEnumerable<DateTime>. To set the value of Times, you call the SetTimes method, which populates the property with five values. However, there is a delay of 500ms between each addition of a DateTime to the Times property, so it takes 2.5 seconds to run. In my Program class, I have a method, PrintTimes, which creates a new Demo object, calls SetTimes, and then prints the value to the screen. Lastly, in my Main method, I call PrintTimes three times—in total, it takes 7.5 seconds to run.
class Program
{
public static void Main()
{
PrintTimes();
PrintTimes();
PrintTimes();
}
private static void PrintTimes()
{
Demo demo = new Demo();
Stopwatch stopwatch = Stopwatch.StartNew();
demo.SetTimes();
foreach (DateTime time in demo.Times)
{
Console.WriteLine(time);
}
stopwatch.Stop();
Console.WriteLine("It took {0} to print out the times", stopwatch.ElapsedMilliseconds);
}
}
class Demo
{
public List<DateTime> Times { get; set; }
public void SetTimes()
{
if (Times == null)
{
Times = new List<DateTime>();
for (int counter = 0; counter < 5; counter++)
{
Thread.Sleep(500);
Times.Add(DateTime.Now);
}
}
}
}
The output from this example code is below. Note that the times printed are constantly changing, and it takes ~2500ms to print out each set of values.
Cache Example
Now I modify the PrintTimes method to incorporate caching by creating an ObjectCache that uses the default MemoryCache instance. I can check if the cache contains an object using the .Contains method, retrieve it with .Get, and add to it using .Add:
private static void PrintTimes()
{
Demo demo;
Stopwatch stopwatch = Stopwatch.StartNew();
ObjectCache cache = MemoryCache.Default;
if (cache.Contains("demo"))
{
demo = (Demo)cache.Get("demo");
}
else
{
demo = new Demo();
demo.SetTimes();
cache.Add("demo", demo, new CacheItemPolicy());
}
foreach (DateTime time in demo.Times)
{
Console.WriteLine(time);
}
stopwatch.Stop();
Console.WriteLine("It took {0} to print out the times", stopwatch.ElapsedMilliseconds);
}
This gives the output:
Note that the time to print for the first iteration is about 60ms longer, but the next two take 0ms, and the values in the three sets are the same.
CacheItemPolicy
In the above example, I only use a default CacheItemPolicy, but you can do a lot more with it:
AbsoluteExpiration: Set a date/time when to remove the item from the cache.ChangeMonitors: Allows the cache to become invalid when a file or database change occurs.Priority: Allows you to state that the item should never be removed.SlidingExpiration: Allows you to set a relative time to remove the item from the cache.UpdateCallback&RemovedCallback: Two events to get notification when an item is removed from the cache. TheUpdateCallbackis called before an item is removed, andRemovedCallbackis called after an item is removed.
Things to Watch Out For
- .NET 4 only ships with support for memory caching out of the box, so you will need to build your own provider if you want any other cache source.
- You must add the
System.Runtime.Caching.dllreference to get access to the namespace and classes. - This is not part of the .NET 4 client profile; it is only in the full .NET 4 Framework. I have logged an issue with Connect to move the assembly into the client profile, and if you agree with this idea, please vote on it: 558309