So far in our series on .NET caching, we've created a basic cache and added a thread to expire objects from the cache based on a fixed expiration date. This may work well in certain situations, where it is possible to predetermine the date of expiration, but what if the date of expiration cannot be predetermined? Perhaps you have an inventory list that should only be expired if the latest shipment has arrived. There is no way to anticipate the exact time of expiration in this case. How can we rearchitect the cache to support the notion of undeterminable temporality?
In this iteration, we will revisit the cache and modify the manner in which expiration is determined so that we can support any complexity for the algorithm used for expiration. We’ll also discuss some advanced threading and performance issues.
Catch up on caching
The first article in our series, "Caching with .NET—when to buy, when to build," described how to use .NET’s built-in cache and provided a walk-through of how to build your own cache, when necessary. The second article, "Create a timed cache with .NET," expanded on the basic cache we developed in the first article by adding the ability to expire items.
Who determines expiration?
In its current form, our cache dictates that any objects placed into it must be accompanied by a fixed date of expiration. Now, we've acknowledged that the date of expiration may not be knowable. Thus, we will remove the dtExpiration parameter from the insertCachedObject method. How, then, can we determine expiration? The cache should query each object inside it to ask whether it is expired—so each object must be able to determine for itself whether it has expired. This means that each object placed into the cache must conform to some standard by which it can be queried. It needs to have a method that can be called to determine expiration status. We can enforce the existence of this method through the use of interfaces.
What is an interface?
An interface is nothing more than a set of methods, properties, and events whose signatures are defined without implementations. Any object that uses the interface must provide those implementations.
An interface is defined using this syntax:
Our cache must be modified so that every object that is added to it exposes a function named isExpired(). The cache will then call this function to determine whether the object is expired. To achieve this, we'll create an interface, which we'll call ICacheable. The interface can be defined in the same file as the CustomCache class or it can be in its own file. Either way, it should be defined in the same namespace as the CustomCache. It's important to understand why the interface must be declared using the Public scope modifier. Without a public declaration, you will get a compiler error informing you that you can't expose a public method (insertCachedObject) that takes ICacheable as a parameter, if the interface is private.
Public Interface ICacheable
Function isExpired() as Boolean
Now, the insertCachedObject method must be modified. Its new form will be as follows:
Public Sub insertCachedObject (key as Object, o as ICacheable)
This breaks the testing code. To fix it, we must modify the code so that the object passed into the insertCachedObject method implements the ICacheable interface. This requires anyone wanting to use the cache to pass an object that has implemented ICacheable. For testing purposes, I have created a class called CacheableString that implements ICacheable, as shown in Listing A.
Finally, we must modify the ReaperThread so that it no longer looks for dtExpiration, but rather calls the isExpired Method. Let’s make a few other improvements while we’re at it.
First, we will modify the cache slightly. In its current implementation, it requires you to modify the hashtable while you are traversing the enumerator, which is inefficient. This will also free you from the need to refresh the enumerator, which is really a hack anyhow. We’ll change this by using a different collection for all expired objects. Once we finish looping through all the objects, we'll perform a SyncLock if any objects need to be removed. The performance improvement will be significant. We will use a queue for this. The SyncLock stays in its current location, since you must prevent the insertCachedObject method from modifying the hashtable.
We’re not through yet—just one more important thing to fix. If you run the previous cache, you’ll notice that the application does not end after the main method is finished. It continues to execute because the clean-up thread is never-ending. We want to change this so that the clean-up thread is a “background” thread whose lifespan depends on the existence of a foreground thread. We can easily accomplish this by setting the clean-up thread’s isBackground property to true. The application will now end after the main method finishes. The code is displayed in Listing B.
Clear our cache
We've looked at how you can use an interface to build flexibility into the cache and how to use a Queue class and a thread’s isBackground property. If you know of any good alternatives to the techniques we looked at in this article, be sure to post them in the discussion so other members can take advantage of them. In the next article, we will enhance the cache with delegates.