Introduction
This article shows how to properly use System.Runtime.Caching.MemoryCache by unifying an inherited Singleton structure.
Background
It is very common for an application to cache objects, whether it is a server side application such as WCF service (Per-Call), a web application (server-side) or even a simple standalone Windows application (WPF/Win-Forms).
.NET 4.0 provides a very simple caching object called System.Runtime.Caching.MemoryCache.
But how do we properly use this object?
From my experience, it is better to unify & simplify these objects into a structured utility that can be used across the application for the following reasons:
- Avoids code duplication across the application
MemoryCache
configuration is unified and can be easily changed - Ease of access to the use of the object for the less experienced programmers on the team
Caching Provider (Base)
Let's wrap the System.Runtime.Caching.MemoryCache using an abstract
base class that contains:
MemoryCache
instance - Locking mechanism
- Errors log
public abstract class CachingProviderBase
{
public CachingProviderBase()
{
DeleteLog();
}
protected MemoryCache cache = new MemoryCache("CachingProvider");
static readonly object padlock = new object();
protected virtual void AddItem(string key, object value)
{
lock (padlock)
{
cache.Add(key, value, DateTimeOffset.MaxValue);
}
}
protected virtual void RemoveItem(string key)
{
lock (padlock)
{
cache.Remove(key);
}
}
protected virtual object GetItem(string key, bool remove)
{
lock (padlock)
{
var res = cache[key];
if (res != null)
{
if (remove == true)
cache.Remove(key);
}
else
{
WriteToLog("CachingProvider-GetItem: Don't contains key: " + key);
}
return res;
}
}
#region Error Logs
string LogPath = System.Environment.GetEnvironmentVariable("TEMP");
protected void DeleteLog()
{
System.IO.File.Delete(string.Format("{0}\\CachingProvider_Errors.txt", LogPath));
}
protected void WriteToLog(string text)
{
using (System.IO.TextWriter tw = System.IO.File.AppendText(string.Format("{0}\\CachingProvider_Errors.txt", LogPath)))
{
tw.WriteLine(text);
tw.Close();
}
}
#endregion
}
Global Caching Provider
Next, I will create an example of a global application cache, which is used for common caching activities of simply caching an object and fetching and removing the object.
public interface IGlobalCachingProvider
{
void AddItem(string key, object value);
object GetItem(string key);
}
The Global Caching Provider inherits from the CachingProviderBase
, and exposes an interfaced singleton:
public class GlobalCachingProvider : CachingProviderBase, IGlobalCachingProvider
{
#region Singleton
protected GlobalCachingProvider()
{
}
public static GlobalCachingProvider Instance
{
get
{
return Nested.instance;
}
}
class Nested
{
static Nested()
{
}
internal static readonly GlobalCachingProvider instance = new GlobalCachingProvider();
}
#endregion
#region ICachingProvider
public virtual new void AddItem(string key, object value)
{
base.AddItem(key, value);
}
public virtual object GetItem(string key)
{
return base.GetItem(key, true);
}
public virtual new object GetItem(string key, bool remove)
{
return base.GetItem(key, remove);
}
#endregion
}
Using the Code
For the sake of simplicity, I will demonstrate the use of System.Runtime.Caching.MemoryCache by a basic WPF application.
The application is caching a text message and fetching the message only when the presentation object is created. The presentation object is completely disconnected from the object storing the message, and even does not have to exist when the message is being stored.
The process is as follows:
The code behind is as follows:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
pesenterCombobox.Items.Add("Message-Box");
pesenterCombobox.Items.Add("Text-Block");
pesenterCombobox.Items.Add("List-Box");
pesenterCombobox.SelectedIndex = 0;
}
private void StoreBtn_Click(object sender, RoutedEventArgs e)
{
string text = new TextRange(inputTextBox.Document.ContentStart,
inputTextBox.Document.ContentEnd).Text;
GlobalCachingProvider.Instance.AddItem("Message", text);
}
private void PresentBtn_Click(object sender, RoutedEventArgs e)
{
var message = GlobalCachingProvider.Instance.GetItem("Message") as string;
switch (pesenterCombobox.SelectedIndex)
{
case 0:
{
MessageBox.Show(message);
break;
}
case 1:
{
contentPresenter.Content = new TextBlock() { Text = message };
break;
}
case 2:
{
if (message != null)
{
var listbox = new ListBox();
var lines = message.Split('\n');
foreach (var ln in lines)
listbox.Items.Add(new ListViewItem() { Content = ln, Height = 16 });
contentPresenter.Content = listbox;
}
break;
}
}
}
}
Further Discussion
The idea behind the article is offering a way unify & simplify your code using application utilities. When we are planning an application, we should modularize our code in order to achieve:
- Flexibility in case of future changes
- Easy maintenance
Modularize code is unified, simple and easy to change, it's also easy to maintain because every module is a small part of the system. Instead of searching for the bug in, say 1,000 lines of code (LOC) you only need to check ~100 LOC.
Modularize application should often be using strong, unified & simplified utilities. For example:
- ThreadInvoker - enables us to simplify and change the use of threads.
LoggerManager
- enables us to simplify and change the use of loggers.DataAccessManager
- enables us to simplify and change the access to data: sometimes it could be access to Database
and sometimes WCF services or both.... and we can change it without touching the business-logic code! - Etc.
History
- 11 May 2014 - Adding "Further Discussion" section