About ASP.NET-object
Cache surely every web developer on the .NET platform knows. It's not at all strange, because this is the only solution for caching web application data in ASP.NET available directly from the box.
Rather functional and lightweight, equipped with mechanisms for priority, crowding out, dependencies and callbacks, Cache is well suited for small applications, working inside the AppDomain. It seems that Microsoft has provided everything that is needed ... But I, nevertheless, want to make it a little better. What exactly?
Sync Updates
Cached data, like much in our world, tends to lose relevance over time. Therefore, after the allotted time interval expires or when one of the dependencies changes, the saved item will disappear from the cache, and we will have to put it there again. MSDN
will tell us how to do this, and we will write this:
')
List <Product> products;
products = ( List <Product>)Cache[ "Products" ];
if (products == null )
{
products = db.Products.ToList();
Cache.Insert( "Products" , products);
}
* This source code was highlighted with Source Code Highlighter .
Everything looks right, but exactly as long as we are not aware that the code can be executed simultaneously in several threads. And in these few lines we have just organized the classic race condition. Of course, nothing terrible will happen, just the cache entry will be updated several times, and each time we will go to the database. But this is an extra job, and it can be avoided by applying the usual double-check blocking. Like this:
private static object _lock = new object ();
...
object value;
if ((value = Cache[ "Products" ]) == null )
{
lock (_lock)
{
if ((value = Cache[ "Products" ]) == null )
{
value = db.Products.ToList();
Cache.Insert( "Products" , value);
}
}
}
var products = ( List <Product>)value;
* This source code was highlighted with Source Code Highlighter .
Thus, we guarantee that only one thread will go to the database for a list of goods, and the rest will wait for its return. You can write such code whenever we work with the cache, but it is better to implement the extension of the Cache object using the extension method.
So,
public static T Get<T>( this Cache cache, string key, object @lock, Func<T> selector,
DateTime absoluteExpiration)
{
object value ;
if (( value = cache.Get(key)) == null )
{
lock (@lock)
{
if (( value = cache.Get(key)) == null )
{
value = selector();
cache.Insert(key, value , null ,
absoluteExpiration, Cache.NoSlidingExpiration,
CacheItemPriority.Normal, null );
}
}
}
return (T) value ;
}
* This source code was highlighted with Source Code Highlighter .
If an item with the specified key is found in the cache, the method will simply return it, otherwise it will lock and perform the download. Constructs
value = Cache.Get(key)
are needed in order not to get the same race when deleting a cache item in another thread. Now, to get our list of products, we can write only one line, and the rest will take over the rest. Overload can be added to taste :)
private static object myLock = new object() ;
...
var products = Cache.Get< List <Product>>( "Products" , myLock,
() => db.Products.ToList(), DateTime .Now.AddMinutes(10));
* This source code was highlighted with Source Code Highlighter .
So, we dealt with one task, but there is still something interesting. For example, a situation when it is necessary to declare several related cache elements invalid at once. ASP.NET Cache provides us with the ability to create dependencies on one or more elements. Like that:
string [] dependencies = { "parent" };
Cache.Insert( "child" , someData,
new CacheDependency( null , dependencies));
* This source code was highlighted with Source Code Highlighter .
And when the
parent element is updated, the
child element will be deleted. Nothing like yet? Well, a little more code, and we will have a full-fledged ...
Tag and group invalidation support
The tagging subsystem will work quite transparently, using the already described key dependency mechanism in the cache. Such keys will be - guess what? - tags. When adding an item to the cache with a certain collection of tags, we will create an appropriate number of keys in the cache and dependence on them.
public static CacheDependency CreateTagDependency(
this Cache cache, params string [] tags)
{
if (tags == null || tags.Length < 1)
return null ;
long version = DateTime .UtcNow.Ticks;
for ( int i = 0; i < tags.Length; ++i)
{
cache.Add( "_tag:" + tags[i], version, null ,
DateTime .MaxValue, Cache.NoSlidingExpiration,
CacheItemPriority.NotRemovable, null );
}
return new CacheDependency( null , tags.Select(s =>
"_tag:" + s).ToArray());
}
* This source code was highlighted with Source Code Highlighter .
Here, as the value of the tag version, I use the current time, as recommended in the
article on Memcached , but in our case we don’t have to compare anything, ASP.NET will do this. Now, when adding an item to the cache, we can easily create a dependency on the tags we specified.
ache.Insert( "key" , value , ache.CreateTagDependency( "tag1" , "tag2" ));
* This source code was highlighted with Source Code Highlighter .
It remains quite a bit - to ensure the reset of such a group of elements in the cache. To do this, you just need to update the cache elements that represent the tags of interest. Everything else happens by itself.
public static void Invalidate( this Cache cache, params string [] tags)
{
long version = DateTime .UtcNow.Ticks;
for ( int i = 0; i < tags.Length; ++i)
{
cache.Insert( "_tag:" + tags[i], version, null ,
DateTime .MaxValue, Cache.NoSlidingExpiration,
CacheItemPriority.NotRemovable, null );
}
}
* This source code was highlighted with Source Code Highlighter .
Notice that the method that creates the tag dependency uses the
cache.Add
method, and here is the
cache.Insert
. The essential difference between these otherwise very similar methods is that the first writes information to the cache only if the specified key was not created earlier, and the second writes it in any case, overwriting the old data. This distinction is very important in our case, because when we simply add an item to the cache, we do not need to update the already existing tags.
On this, it seems, and all ...
I demand the continuation of the banquet!
And there is still much to continue. For example, you can modify the Get method described here so that, instead of immediately deleting, the cache data temporarily “moved” to another cell, and instead of blocking, return the requested information from it while new data is loaded into the cache.
Instead of extension-methods, you can make a certain abstraction of the caching provider and work with any storage without changing the application code or completely disable the cache when debugging, use IoC ... you never know!
And I hope that the approaches described in my article will be useful for you;)
UPDATE : Looking with my own eye on my own code, I saw one bad thing in it - the lock during synchronization was set for the entire cache. So I modified the extension-method
Get to accept a custom object for blocking.