2. Adapdev.Cache

The Adapdev.Cache namespace provides advanced methods for caching objects to memory and persistent stores. It also allows for several different "scavenger" strategies - ways to clean the cache - such as LIFO and absolute time expiration.

2.1. The ICache Interface

All cache implementations inherit from the ICache interface which defines methods for insertion and retrieval. To add an object to the cache, just pass in the id and object. Whenever possible, use the ICache interface in your programs so that you can easily switch between implementations.

2.1.1. Adding objects to the cache

To add an object to a cache, just pass in the object and its id.

Order o = new Order();
o.OrderId = 1;
... // do something

ICache cache = new MutableInMemoryCache();
cache.Add(o.OrderId, o);

The id must be unique only to that object type. The caching engines automatically handle assignments. So, the following works, even though Order and Product have the same id:

Order o = new Order();
o.OrderId = 1;
... // do something

Product p = new Product();
p.ProductId = 1;

ICache cache = new MutableInMemoryCache();
cache.Add(o.OrderId, o);
cache.Add(p.ProductId, p);

This is powerful because it avoids conflicts between different objects within the cache. The chances of having multiple objects with the same numeric id - especially when using a backend database with autoincrementing values - is quite high. This model accomodates for duplicate ids across objects.

2.1.2. Replacing objects in the cache

To replace an object, just add the new object using an existing id. The current cache item will be replaced.

Order o = new Order();
... // do something

ICache cache = new MutableInMemoryCache();
cache.Add(1, o); // add using id #1

Order o2 = new Order();
cache.Add(1, o2); // add using id #1 - replaces current object

2.1.3. Getting an object out of the cache

To get an object out of the cache, pass in the object type and id.

Order o = new Order();
... // do something

ICache cache = new MutableInMemoryCache();
cache.Add(1, o);

Order o2 = cache.Get(typeof(Order), 1); // gets the object from the cache

Important

You must pass in the exact object type to retrieve it from the cache. For example, if Order inherits from IOrder, using IOrder won't work for retrieval since the caching engine is looking for an Order object specifically.

2.1.4. Removing an object from the cache

To remove an object from the cache, pass in the object type and id.

Order o = new Order();
... // do something

ICache cache = new MutableInMemoryCache();
cache.Add(1, o);

cache.Remove(typeof(Order), 1); // removes the object from the cache

2.1.5. Clearing the cache

Clearing the cache will remove all items that are currently in the cache

Order o = new Order();
... // do something

ICache cache = new MutableInMemoryCache();
... // add objects

cache.Clear();

2.1.6. Populating the cache

Certain cache implementations, such as file system caching, need to be populated in order to load all cache entries. To do this, just call the Populate() method.

ICache cache = new FileCache();
cache.Populate(); // loads all cache items from the file store

2.2. ICache Implementations

2.2.1. Saving to Memory - MutableInMemoryCache

The most performance friendly way to cache items is in memory. The MutableInMemoryCache will store objects in memory. The only issue with it is that it's mutable - that means that anytime you make a change to an object, if it's in the cache, the cache instance will change, too. Here's an example:

// Create your object
Order o = new Order();
o.OrderId = 1;
o.CustomerName = "Joe Schmoe";

// Add it to the cache
ICache cache = new MutableInMemoryCache();
cache.Add(o.OrderId, o);

// Change your object
o.CustomerName = "John Doe";

Order cachedOrder = cache.Get(typeof(Order), 12) as Order;
Assert.AreEqual(cachedOrder.CustomerName, "John Doe"); // passes - cache changed

As you can see, the information in the cache changed as the object changed. This is fine in some instances, but in other instances you'll want the cache to be a snapshot that doesn't change. For that use the ImmutableInMemoryCache.

2.2.2. Saving to Memory - ImmutableInMemoryCache

In contrast to the MutableInMemoryCache, ImmutableMemoryCache takes a snapshot of an object when it's added and breaks the reference between the user object and the cache object. This prevents unintended changes from occuring.

// Create your object
Order o = new Order();
o.OrderId = 1;
o.CustomerName = "Joe Schmoe";

// Add it to the cache
ICache cache = new ImmutableInMemoryCache();
cache.Add(o.OrderId, o);

// Change your object
o.CustomerName = "John Doe";

Order cachedOrder = cache.Get(typeof(Order), 12) as Order;

// fails
Assert.AreEqual(cachedOrder.CustomerName, "John Doe"); 

// passes - cache didn't change
Assert.AreEqual(cachedOrder.CustomerName, "Joe Schmoe"); 

In the above example, the object in the cache did not change, even though the user object did change. ImmutableInMemoryCache is more performance intensive, but if you need that snapshot capability than you should use it.

2.2.3. Saving to the File Store - FileCache

The FileCache saves all CacheItems to the underlying file store.

Warning

Saving items to the file store is slower than in-memory options.

// Create your object
Order o = new Order();
o.OrderId = 1;
o.CustomerName = "Joe Schmoe";

// Add it to the cache
ICache cache = new FileCache();
cache.Add(o.OrderId, o);

// Change your object
o.CustomerName = "John Doe";

Order cachedOrder = cache.Get(typeof(Order), 12) as Order;

// fails
Assert.AreEqual(cachedOrder.CustomerName, "John Doe"); 

// passes - cache didn't change
Assert.AreEqual(cachedOrder.CustomerName, "Joe Schmoe"); 

By default, FileCache uses the current assemblies directory for its location and creates a filecache folder. For example, if your .exe is located in C:\MyApp, the FileCache will create C:\MyApp\filecache and store all data there. To override the default location, pass the folder location into the constructor.

ICache cache = new FileCache(@"C:\MyApp\SpecialFolder");

Now the cache location will be C:\MyApp\SpecialFolder\filecache.

2.3. The ICacheItem Object

The ICache interface stores objects as ICacheItem objects internally. The ICacheItem wraps the actual object with information to include when it was created, its ordinal value and Type. At any time you can retrieve an ICacheItem to get any relevant information.

Getting an ICacheItem

Order o = new Order();
... // do something

ICache cache = new MutableInMemoryCache();
cache.Add(1, o);

ICacheItem cacheItem = cache.GetCacheItem(typeof(Order), 1);
Console.WriteLine(cacheItem.Created); // when it was added
Console.WriteLine(cacheItem.LastAccessed); // when it was last accessed
Console.WriteLine(cacheItem.Key); // the internal key
Console.WriteLine(cacheItem.ObjectType.FullName); // the object type
Console.WriteLine(cacheItem.Ordinal.ToString()); // it's ordinal position

Order o2 = cacheItem.Object as Order; // gets the actual object

Getting all CacheItems

... // create a cache
foreach(ICacheItem ci in cache.Entries)
{
   // do something
}

2.4. CacheManager

CacheManager implements the Singleton pattern and allows for one centralized instance of a cache. In most instances, you'll want to use the CacheManager vs. instantiating your own cache objects.

2.4.1. Setting and Retrieving the ICache

CacheManager.SetCache(CacheType.File); // use the file store
ICache cache = CacheManager.Cache; // retrieve the cache

Because the CacheManager is a Singleton you can use it anywhere in your program and it will always use one central cache. You don't even have to set the CacheType...by default it uses the ImmutableInMemoryCache.

2.4.2. Changing the CacheType

You can switch to a different CacheTypes midstream. However, bear in mind that ALL programs are using the same cache implementation.

CacheManager.SetCache(CacheType.File); // use the file store
ICache cache = CacheManager.Cache; // uses the file store

... // do some work

CacheManager.SetCache(CacheType.MutableInMemory); // switch to in-memory
ICache cache = CacheManager.Cache; // now using the in-memory cache

Note

By default, when you change CacheTypes all existing CacheItems will be copied to the new cache. In the example above, all of the file store CacheItems would now be copied over to the new in-memory cache.

2.4.3. Changing the CacheType without Copying Values

As noted above, switching the cache midstream automatically copies values over. To disable this, use the overriden SetCache method.

CacheManager.SetCache(CacheType.File); // use the file store
ICache cache = CacheManager.Cache; // uses the file store

... // do some work

// switch to in-memory - DOESN'T copy values
CacheManager.SetCache(CacheType.MutableInMemory, false); 
ICache cache = CacheManager.Cache; // now using the in-memory cache

You can also just set the Cache property.

Note

Setting the Cache property does not copy values. To ensure values are copied, use the SetCache method.

CacheManager.SetCache(CacheType.File); // use the file store
ICache cache = CacheManager.Cache; // uses the file store

... // do some work

// switch to in-memory - DOESN'T copy values
CacheManager.Cache = new MutableInMemoryCache(); 
ICache cache = CacheManager.Cache; // now using the in-memory cache

2.5. CacheUtil

CacheUtil allows you to copy values from one ICache to another.

... // create two cache instances and populate them

// Copy all values from cache1 to cache2
CacheUtil.Copy(cache1, cache2); 

2.6. CacheStats

The CacheStats class allows for diagnostics on a specific cache. It implements the Decorator pattern. To use it, pass in an existing ICache object into the constructor. CacheStats is an ICache, so you can use it just like any other ICache object.

ICache cache = CacheManager.Cache;

CacheStats stats = new CacheStats(cache);
stats.Add(1, new Order()); // adds the Order object to cache

// show how long an insert takes
Console.WriteLine(stats.InsertTime.ToString());

// retrieve an object
Order o = stats.Get(typeof(Order), 1) as Order;

// show how long it takes to retrieve an object
Console.WriteLine(stats.RetrieveTime.ToString());

// show all info
Console.WriteLine(stats);

// Output:
Insert Time: 0.00433225091624703
Avg. Insert Time: 0.00433225091624703
Retrieve Time: 0.00130173980868958
Avg. Retrieve Time: 0.00130173980868958
Hit Count: 1
Miss Count: 0

CacheStats provides information on

  • Insert Time - the last insertion time

  • Avg Insert Time - the average insertion time

  • Retrieve Time - the last retrieve time

  • Avg Retrieve Time - the average retrieve time

  • Hit Count - how many cached items were found and retrieved

  • Miss Count - how many cached items were not found

2.7. The IScavenger Interface

Scavengers allow you to purge data from the cache utilizing different strategies. Any ICache implementation will accept any IScavenger implementation. Below is a simple example of a cache being purged using a sliding expiration, which will remove any cache items that haven't been accessed within the given timespan.

ICache cache = CacheManager.Cache;
... // populate the cache

// Remove all cache items that haven't been accessed within the past hour
IScavenger scavenger = new SlidingExpirationScavenger(new TimeSpan(1,0,0,0));
cache.Scavenge(scavenger);

2.8. IScavenger Implementations

2.8.1. Absolute Expiration

When an object is cached, it is stamped with a creation date and time. Absolute expiration will remove all items that were created before the given DateTime.

ICache cache = CacheManager.Cache;
... // populate the cache

// Remove all cache items that were created before today
IScavenger scavenger = new AbsoluteExpirationScavenger(DateTime.Today);
cache.Scavenge(scavenger);

2.8.2. First In First Out

First In First Out (FIFO) will remove the first X inserted items from the cache.

ICache cache = CacheManager.Cache;
... // populate the cache

// Remove the first 10 inserted items
IScavenger scavenger = new FIFONumberScavenger(10);
cache.Scavenge(scavenger);

2.8.3. Greater Than a Certain Ordinal Position

When an object is cached, it receives an ordinal, auto-incremented location in the cache. Greater Than Ordinal will remove all cache items that have an original ordinal number greater than the supplied number.

There's a subtle difference between this and the LIFO / FIFO implementations. As objects are added and removed from the cache, there may be gaps in the ordinal numbers. For example, you could add 10 objects the cache, and then remove 5 leaving you a total of 5. The ordinal numbers of the 5 cache entries might be 2, 3, 6, 8, 9. This is because ordinals 1, 4, 5, 7 and 10 were removed. As you can see the ordinals do not readjust with every cache removal, but remain static. If you use a greater than expiration of 8 in the above example, only the last entry would be removed (9).

In contrast, FIFO and LIFO take the designated block out of the cache. So, if you did a LIFO removal of 8, it would remove all of the cache entries, since only 5 remain and its removing 8.

ICache cache = CacheManager.Cache;
... // populate the cache

// Removes any cache items with an original ordinal position of 11 or higher
IScavenger scavenger = new GreaterThanOrdinalScavenger(10);
cache.Scavenge(scavenger);

2.8.4. Last In First Out

Last In First Out (LIFO) will remove the last X inserted items from the cache.

ICache cache = CacheManager.Cache;
... // populate the cache

// Removes the last 10 inserted items
IScavenger scavenger = new LIFONumberScavenger(10);
cache.Scavenge(scavenger);

2.8.5. Sliding Expiration

When a object in the cache is accessed, the LastAccessed property is updated. Sliding expiration will remove all items that haven't been accessed within the given time period. Absolute expiration, in contrast, focuses on the DateTime that the object was added to the cache - not last accessed.

ICache cache = CacheManager.Cache;
... // populate the cache

// Removes all items that haven't been accessed within the last hour
IScavenger scavenger = new SlidingExpirationScavenger(new TimeSpan(1,0,0,0));
cache.Scavenge(scavenger);