Back in February 2013, I wrote an article on caching in .Net applications. At that time, I was using the caching mechanism available from System.Web even if the application was not a web application.
Since the .Net Framework 4, we now have access to another caching mechanism that doesn’t require a reference to System.Web. This newer mechanism is the MemoryCache class which is inheriting from the ObjectCache class.
Available source code
The downloadable demo solution has been created using Visual Studio 2019. Both VB and C# code are available. Even if the solution has been created using VS2019, the code can be used in previous versions if you are targeting at least the .Net framework 4.
Why caching?
The main reason why you want to implement caching in your application is performance. Our applications are often calling the same methods again and again to get some values that are not changing. These values can come from a database or from a service on the web.
So instead of querying the database, or querying the service, or doing the same long calculation for the same values repeatedly, some memory is used to cache values to improve the performance of your application.
Reference required
In order to be able to use the MemoryCache class, you will need to add a reference to the System.Runtime.Caching.dll file as shown in figure 1.
Figure 1: Adding a reference
The demo UI
The demo application of this month is very similar to the one from the previous article. A few textboxes, a few buttons, and a grid to display results.
Figure 2: The demo application in action
Adding and/or updating an item in the cache
We will first talk about adding and updating values in the cache. In this demo, I store strings in the cache because it is easier to spit out the values in a grid but the value in the cache is an object, it can store absolutely anything.
As you can see in the following code snippet, when you click the Set button from the test UI, the click event of the button calls the AddUpdateCacheItem method passing the content of the Key and Value textboxes, the value 60 and an empty string (explanation on the last two arguments later).
Private Sub BtnSet_Click(sender As Object, e As EventArgs) Handles btnSet.Click AddUpdateCacheItem(txtPutKey.Text, txtPutValue.Text, 60, String.Empty) End Sub Private Sub AddUpdateCacheItem(ByVal pKey As String, ByVal pValue As String, ByVal pExpiration As Double, ByVal pFilePath As String) If String.IsNullOrWhiteSpace(pKey) Then MessageBox.Show("You need to fill the Key") Return End If If String.IsNullOrWhiteSpace(pValue) Then MessageBox.Show("You need to fill the Value") Return End If Dim policy As CacheItemPolicy = New CacheItemPolicy() If pExpiration > 0 Then policy.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(pExpiration) If Not String.IsNullOrWhiteSpace(pFilePath) Then policy.ChangeMonitors.Add(New HostFileChangeMonitor({pFilePath})) policy.UpdateCallback = New CacheEntryUpdateCallback(AddressOf OnCacheUpdated) _cache.[Set](pKey, pValue, policy) RefreshOutputCache() End Sub Private Sub RefreshOutputCache() dataGridView1.DataSource = _cache.Cast(Of DictionaryEntry)().ToList() End Sub
private void BtnSet_Click(object sender, EventArgs e) { AddUpdateCacheItem(txtPutKey.Text, txtPutValue.Text, 60, string.Empty); } private void AddUpdateCacheItem(string pKey, string pValue, double pExpiration, string pFilePath) { if (string.IsNullOrWhiteSpace(pKey)) { MessageBox.Show("You need to fill the Key"); return; } if (string.IsNullOrWhiteSpace(pValue)) { MessageBox.Show("You need to fill the Value"); return; } CacheItemPolicy policy = new CacheItemPolicy(); if (pExpiration > 0) policy.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(pExpiration); if (!string.IsNullOrWhiteSpace(pFilePath)) policy.ChangeMonitors.Add(new HostFileChangeMonitor(new[] {pFilePath})); policy.UpdateCallback += OnCacheUpdated; _cache.Set(pKey, pValue, policy); RefreshOutputCache(); } private void RefreshOutputCache() { dataGridView1.DataSource = _cache.Cast<DictionaryEntry>().ToList(); }
The Key and Value arguments are validated to ensure they are not empty.
Then, a CacheItemPolicy instance is created. If a value is passed to the pExpiration argument, it is used to set the policy’s number of seconds after which the cached item will automatically expired and removed from memory. In the demo application, the value is 60 is always passed. If you pass a value of 0, the cached item will just never expire. The pFilePath argument will be covered later. We are also setting the UpdateCallback delegate to the OnCacheUpdated method. More details about it in the next section.
The Set method of the cache object is called passing the key, the value and the policy. Notice that we don’t need to do anything special to check if the value is existing or not. If an item with the same key exists, it will be updated with the new value, otherwise it will just be created.
Finally, the Refresh OutputCache is called just to display the content of the cache into the grid.
A word on UpdateCallback
When adding a new item to the cache in the previous section, I have set the expiration value to 60 seconds.
The OnCacheUpdated method listed here is only showing a message explaining what action happened to which key. In a real application, this is not very useful. I have added this code to prove that the expiration is actually working.
Private Sub OnCacheUpdated(ByVal arguments As CacheEntryUpdateArguments) MessageBox.Show($"item {arguments.Key} has {arguments.RemovedReason}") End Sub
private void OnCacheUpdated(CacheEntryUpdateArguments arguments) { MessageBox.Show($"item {arguments.Key} has {arguments.RemovedReason}"); }
You might find that the OnCacheUpdated method will not be called exactly 60 seconds after the item has been added to the cache. The reason is performance. There is a kind of garbage collector running every 20 seconds by default to clean any expired items. But after an item has expired, even if the event has not been triggered yet, if you try to get the value from an expired item, you will not get anything since the item does not exist anymore.
Removing an item from the cache
Manually removing an item from the cache is very easy. You just need to call the Remove method of your cache object passing the key. The following snippet does exactly this when you click the Remove button.
Private Sub BtnRemove_Click(sender As Object, e As EventArgs) Handles btnRemove.Click If String.IsNullOrWhiteSpace(txtPutKey.Text) Then MessageBox.Show("You need to fill the Key") Return End If If Not _cache.Contains(txtPutKey.Text) Then MessageBox.Show("This Key does not exist") Return End If _cache.Remove(txtPutKey.Text) RefreshOutputCache() End Sub
private void BtnRemove_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtPutKey.Text)) { MessageBox.Show("You need to fill the Key"); return; } if (!_cache.Contains(txtPutKey.Text)) { MessageBox.Show("This Key does not exist"); return; } _cache.Remove(txtPutKey.Text); RefreshOutputCache(); }
Getting an item from the cache
Because the cache is a dictionary, you can get a value from it just like you would do for a dictionary. The following snippet reads the value for a key from the cache and output it in a textbox when you click on the Get button.
Private Sub BtnGet_Click(sender As Object, e As EventArgs) Handles btnGet.Click If String.IsNullOrWhiteSpace(txtGetKey.Text) Then MessageBox.Show("You need to fill the Key") Return End If If Not _cache.Contains(txtGetKey.Text) Then MessageBox.Show("This Key does not exist") Return End If txtGetValue.Text = _cache(txtGetKey.Text).ToString() End Sub
private void BtnGet_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtGetKey.Text)) { MessageBox.Show("You need to fill the Key"); return; } if (!_cache.Contains(txtGetKey.Text)) { MessageBox.Show("This Key does not exist"); return; } txtGetValue.Text = _cache[txtGetKey.Text].ToString(); }
Listing items from the cache
The List button just refreshes the content of the grid with the current content of the cache. This is done by calling the very same RefreshOutputCache method that was listed in “Adding and/or updating an item in the cache“.
You might notice some items in your grid showing some keys containing the word “sentinel”. These are internal items created for the expiration policy. Just ignore them.
From what I have read, enumerating a cache is a very costly operation. I do it here for demoing purpose but in a real application, try not to abuse from it specially if your cache contains a lot of large items.
Detecting changes on a file and MS SQL databases
One great feature of this class is the fact that it can monitor for changes in a file. If you remember, my AddUpdateCacheItem method has an argument named pFilePath which I said I would give more details later. So now is the time.
When we add an item to the cache, we can specify in its policy to expire the item whenever the file gets changed (or deleted). This is done by creating an instance of the HostFileChangeMonitor class with the full path of the file.
Private Sub BtnFileChangeMonitor_Click(sender As Object, e As EventArgs) Handles btnFileChangeMonitor.Click Dim fileContents As String = TryCast(_cache("filecontents"), String) If Not String.IsNullOrWhiteSpace( fileContents ) Then MessageBox.Show("The file is already loaded and didn't change since it was loaded") Return End If Dim strCachedFilePath As String = Path.Combine(Application.StartupPath, "cacheText.txt") If Not File.Exists(strCachedFilePath) Then MessageBox.Show($"The file does not exist ({strCachedFilePath})") Return End If Dim strfileContents As String = File.ReadAllText(strCachedFilePath) AddUpdateCacheItem("filecontents", strfileContents, 60, strCachedFilePath) End Sub
private void BtnFileChangeMonitor_Click(object sender, EventArgs e) { string fileContents = _cache["filecontents"] as string; if (fileContents != null) { MessageBox.Show("The file is already loaded and didn't change since it was loaded"); return; } string strCachedFilePath = Path.Combine(Application.StartupPath, "cacheText.txt"); if (!File.Exists(strCachedFilePath)) { MessageBox.Show($"The file does not exist ({strCachedFilePath})"); return; } string strfileContents = File.ReadAllText(strCachedFilePath); AddUpdateCacheItem("filecontents", strfileContents, 60, strCachedFilePath); }
When a change is detected in the file, the item is automatically expired (and thus) removed from the memory cache but it is not reloaded. Once again for performance reason. So, your code needs to check, much like you would do for any other cache item, if your item is in the cache, use it, otherwise, reload it.
In the demo application, I have added a text file named cacheText.txt to the projects. Whenever the project is compiled, the file is copied to the bin/debug folder. While the demo application is running and after you clicked the “File Change Monitor” button, if you open the file in favorite text editor (like Notepad), modify it and save it, you will automatically get a message box saying that the file was changed.
You can find more information about the HostFileChangeMonitor class from here.
There is also a SqlChangeMonitor class which will expire an item from the cache when the item is modified in the database. This class is simply a wrapper on the ADO.Net SQLDependency class. More information on the SqlChangeMonitor class is available from here. Back in March 2012, I wrote an article on the SQLDependency class.
How do we use the cache then?
So, the memory cache mechanism is there to gain performance by avoiding repeating long calculations that given the same input would give the same result, or by caching results from a database or a cloud service (for example a description for an ID), and so on.
The pattern is always the same. We first need to check if we already have the value for a key (like a process name with its arguments, or an ID). If the key is found, your job is done, all you need to do is to return the cached value. If the key is not found, do whatever you need to do to get your value and store it in the cache for future use along with an expiration time that make sense for your value.
Private Sub BtnFakeLongProcess_Click(sender As Object, e As EventArgs) Handles btnFakeLongProcess.Click Dim strValue As string = LongProcess(5) End Sub Private Function LongProcess(ByVal pFakeID As Integer) As String Dim strKey As String = "longprocess_" & pFakeID Dim strLongProcess As String = TryCast(_cache(strKey), String) If Not String.IsNullOrWhiteSpace(strLongProcess ) Then MessageBox.Show($"The long process result is already loaded: {strLongProcess}") Else strLongProcess = DateTime.Now.ToLongTimeString() AddUpdateCacheItem(strKey, strLongProcess, 60, String.Empty) End If Return strLongProcess End Function
private void BtnFakeLongProcess_Click(object sender, EventArgs e) { string strValue = LongProcess(5); } private string LongProcess(int pFakeID) { string strKey = "longprocess_" + pFakeID; string strLongProcess = _cache[strKey] as string; if (strLongProcess != null) { MessageBox.Show($"The long process result is already loaded: {strLongProcess}" ); } else { strLongProcess = DateTime.Now.ToLongTimeString(); AddUpdateCacheItem(strKey, strLongProcess, 60, string.Empty); } return strLongProcess; }
Why set the expiration on cached values?
You might be tempted to pass 0 as the value the pExpiration argument for all your cached values but you should really find an acceptable value for your processes. In this demo application, I am using 60 seconds just because I want to demonstrate that values eventually go out of scope and they are removed from memory. Expiring values from the cache will help free some memory.
So probably that 60 seconds is a bit short, but infinite is surely way too long!
A nicer way to implement caching
A few weeks ago, I had to renew my PostSharp license. I opened their website to find out what was new and saw that they are now supporting caching just by using attributes. This can be a wonderful alternative to having to write all that code by yourself.
Conclusion
Caching can really improve the speed of your application if you always hit the database or a service for the same values that very don’t change that often. It is simple enough to put in place and the benefits are immediate.
Of course, the MemoryCache has a lot more options than what is covered here. But at least, this article should get you started quickly.