©2006 Alexander Homer – alex@stonebroom.com
This series of articles looks at the patterns & practices Enterprise Library tools, and how you can use them in your ASP.NET applications. The five articles are:
· Part 1 - An Introduction to Enterprise Library
· Part 2 - Working with Enterprise Library in ASP.NET
· Part 3 - Using Enterprise Library in ASP.NET Partial Trust Mode
· Part 4 - Extending Enterprise Library with Custom Providers
· Part 5 - Adding Configuration Design Support to a Custom Provider
Enterprise Library from the Microsoft patterns & practices (p&p) group provides a range of application blocks and features that make it easier to accomplish complex tasks such as accessing databases, caching and encrypting data, handling exceptions, and generating Event Log messages. It demonstrates best practice in design and implementation, and is supplied in source code form so that you can learn from it, and also adapt it to suit your own specific requirements.
One of the most useful features in terms of adaptability is the fact that all of the application blocks are fully extensible. Like ASP.NET itself, Enterprise Library implements a pluggable architecture that allows you to replace parts of the code with your own implementations, or extend the application blocks by creating your own providers. Figure 1 shows a schematic view of how the application blocks rely on services exposed by the Enterprise Library core (such as configuration, instrumentation, and object creation services). In addition, each block uses one or more pluggable providers to connect to the resources or data it uses or processes.

Figure 4 - The pluggable architecture of Enterprise Library uses replaceable providers
As an example of this architecture within an application block, Figure 2 shows how the Caching Application Block uses a series of separate classes to cache and expose data. The core operations of the block take place through a Cache Manager, which exposes the public methods available to client applications. Cached data resides in an in-memory cache, providing best performance when reading and storing data. At the same time, all changes to the cached data are passed to a backing store provider, which persists the data into the chosen cache backing store.

Figure 2 - The architecture of the Caching Application Block
The Caching Application Block ships with three backing store provider implementations. The Isolated Storage provider encrypts data and stores it on disk within the current user's profile folders. The Database Backing Store provider stores it in a database table. The Null Backing Store provider does not store the cached data in a persistent format, so that the caching mechanism relies only on the in-memory cache - an approach that meets some types of caching requirements.
In previous articles about using Enterprise Library in ASP.NET applications (see http://www.daveandal.net/articles/EntLibASPNET/), you saw the use of the Isolated Storage provider - mainly because this is the easiest to configure and is sufficient to demonstrate usage of the Caching Application Block interface and its operation. However, Isolated Storage is not the most ideal technique for server applications like ASP.NET. Because all anonymous users execute the application under the same account, Isolated Storage does not differentiate between users. In addition, the encryption of the data, and the location, means that it is not easily shared across multiple servers in scenarios such as a Web farm installation.
One way round this is to use a central database server, and configure the Caching Application Block to persist its data there, using keys that include a user ID and an application name, or other information that identifies each application or user where this is a requirement (some data you cache may, of course, be common to all users). But using a central database server is not ideal for all applications, and that is where the pluggable architecture enters the picture. If the shipped backing store providers don't meet your needs, you can create your own custom backing store provider that persists the data in exactly the way that best suits your requirements. This is the subject you will explore in this article.
Before you fire up Visual Studio and start writing code for a custom provider, you should consider some of the important points regarding the design and implementation of providers in general. Your custom provider should follow the same design principles as the application blocks wherever this is practical. This includes:
· Adhering to object-oriented design principles
· Making use of appropriate design patterns
· Using resources efficiently
· Applying best practice principles for security, such as distrust of user input and the principle of least privilege
You must also avoid creating a provider that changes the fundamental design aims or the nature of the block, which may affect stability and cause errors outside of the provider. For example, creating a non-symmetric provider for the Cryptography Application Block is likely to affect the way that the block works, because its design only fully supports symmetric algorithms.
In the case of the Caching Application Block, any provider you create must meet the aims described for the block in the Enterprise Library Documentation (available from the Enterprise Library section of your Start menu). For example, the Caching Application Block is designed to perform efficiently and be thread-safe. It also ensures that the backing store remains intact if an exception occurs while it is being accessed, and that the in-memory cache and the backing store remain synchronized at all times. To help meet these aims, your custom cache provider must raise exceptions that the block or the client code can handle if an error occurs that may affect the backing store content or synchronization between the in-memory and persistent caches.
All of the application blocks define an interface for the providers they use, and many contain a base class that you can inherit from when creating a custom provider. The Caching Application Block defines the IBackingStore interface, which contains the following members:
· Count. A read-only integer property that returns the number of objects in the backing store
· Add. This method takes as a parameter a new cache item and adds it to the backing store. This operation must succeed even if an item with the same key already exists. If any part of the process fails, it must remove both the existing and new item from the backing store
· Remove. This method takes as a parameter the (String) key of an existing item, and removes that item from the backing store
· UpdateLastAccessedTime. This method takes as parameters the (String) key of an item and a DateTime instance and updates the last accessed time property of that cached item
· Flush. This method, which takes no parameters, flushes all stored items from the cache
· Load. This method, which takes no parameters, returns a HashTable containing all the items from backing store
The Caching Application Block also contains a base class named BaseBackingStore, which automatically implements the rule on the Add method of the IBackingStore interface by first calling the RemoveOldItem method and then the AddNewItem method in the class that inherits from it. If either of these methods fails, it calls the over-ridden RemoveItem method to ensure cache consistency before throwing an exception to the routines within the application block. You can considerably reduce the amount of code you have to write by using this base class as the starting point for your custom provider.
Figure 3 shows how the methods and property exposed to the client application through the Cache Manager relate to the methods and property of the inter-component interfaces within the block.

Figure 3 - The interaction between the code components in the Caching Application Block
As you can see from Figure 3, the BaseBackingStore class exposes abstract methods that you must over-ride in your provider. The single property and six methods you must implement in your custom backing store provider when inheriting from BaseBackingStore are:
// return the number of objects in the backing store
public override int Count
// add a new item to persistence store
protected override void AddNewItem(int storageKey, CacheItem newItem)
// flush all items from the backing store
public override void Flush()
// load all items from the underlying store without filtering expired items
protected override Hashtable LoadDataFromStore()
// remove an item with the specified storage key from the backing store
// should throw an error if the item does not exist
protected override void Remove(int storageKey)
// remove an existing item with same key as a new item from the persistence store
// should not throw an error if the item does not exist
// called before a new item with the same key is added to the cache
protected override void RemoveOldItem(int storageKey);
// update the last accessed time for the specified cache item
protected override void UpdateLastAccessedTime(int storageKey, DateTime timestamp)
If you need to dispose of managed or un-managed resources, for example by closing or deleting files, you can over-ride the Dispose methods of the BaseBackingStore.
As well as understanding how to interact with the existing classes in Enterprise Library when you design your provider, you must figure out how to handle the data or resources that the application block you are extending uses. In the case of the Caching Application Block, this means deciding where and how you will store the cached data. The fact that you need to do something different from the built-in providers is, of course, the main reason for creating a custom provider for an application block, and so you should already have an idea of where the data will be persisted.
In the case of ASP.NET applications, a common requirement is to cache the data in a location accessible from all servers and all running instances of the application, especially for application-level data that is not user-specific. Alternatively, you might have a requirement that means some specific storage format or technology is involved. In theory, you can cache the data anywhere, and using any format that you wish. Just bear in mind that you must provide a robust mechanism that meets the aims of the block, and which imposes sufficient security over the content.
For this example, the choice is custom format disk files, stored at a location defined when configuring the provider. This location could be on the local machine, or on a network drive. Other approaches that might be suitable, depending on the application's requirements, could include delivering it to a Web Service for remote storage, writing to some otherwise unsupported database system, or even sending it to a remote location through an error-tolerant messaging service.
Remember that the Caching Application Block only reads the persisted data when the application starts, and not when the application reads data (because it reads it from the in-memory cache), so being able to persist it efficiently is more important than access time for reading items. You might consider an asynchronous process that takes the data and persists it while allowing the application to continue running, though you would need to ensure that your code is robust and fault tolerant, and can correctly queue cache updates.
Also bear in mind that, because the Caching Application Block only reads from its in-memory cache (and not the persisted cache) during its lifetime, using a single central cache for multiple instances of the block will not work if you retain a reference to the block in your application. If you run two instances of the ASP.NET example within Visual Studio 2005 or Visual Web Developer, you will see that one instance cannot see cached items from other instance due to the way that the IDE runs the code. However, if you install the example into IIS and run two instances, you will see that they share the cache and can retrieve cached items created by the other instance. This is because, due to the stateless nature of HTTP, each page load creates a new instance of the Caching Application Block - which then loads the current set of cached items from the backing store.
The custom provider described here is simplified so that you can concentrate on the interaction with the Caching Application Block and the overall requirements of Enterprise Library. As you will see, this means that some features are not supported. For example, it does not support cache refresh callbacks, variable cache priorities, and multiple expirations. Instead, it caches all items with the normal priority and with the sliding time expiration policy. It is not difficult to implement these missing features, but does involve quite a lot of extra code - in particular for handling all the different expiration object types - whereas the aim of the example is to understand the principles for creating a custom provider that integrates with the appropriate application block.
When the Caching Application Block calls the methods of the example provider to cache data, it will create two disk files in the folder specified during configuration. The first is a binary file containing a serialized representation of the cached value; with the name cache-key.cachedata (where cache-key is the integer cache key value as a string). The second file is the "information" file containing the metadata about the cached item - the string key name, the date and time the value was last updated, and the expiry period as a sliding time. This file is named cache-key.cacheinfo (where cache-key is the integer cache key value as a string).
Each cached value will use its own two files, meaning that adding items to the cache does not involve reading and updating what could be a very large file if all the items were stored in the same file. It also makes it easy to count the number of cached items, and delete individual items (by simply deleting the relevant pair of files).
You can create a custom provider in a new project, and compile it into a separate assembly, or you can build the provider within the Enterprise Library solution in Visual Studio and compile it into the existing assemblies. This second option is often easier, because the solution you use already references all the required assemblies and namespaces, and your provider is easy to configure afterwards when you use it in your applications.
If you decide to use a new and separate project, you must add references to the following assemblies (located in the %Program Files%\Microsoft Enterprise Library January 2006\bin folder) to your project:
· Microsoft.Practices.EnterpriseLibrary.Caching.dll
· Microsoft.Practices.EnterpriseLibrary.Common.dll
· Microsoft.Practices.ObjectBuilder.dll
Then, in your custom provider class, you will generally need to import the following namespaces:
using System;
using System.IO;
using System.Collections;
using System.Collections.Specialized;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Caching;
using Microsoft.Practices.EnterpriseLibrary.Caching.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Caching.BackingStoreImplementations;
In order for the class for your custom provider to appear in the Configuration Console as a custom cache backing store, and be installable in the Caching Application Block, it must implement the IBackingStore interface and carry a ConfigurationElementType attribute indicating that it implements the class CustomCacheStorageData. The class BaseBackingStore implements IBackingStore, so inheriting from this satisfies the first condition. The listing below shows how the class carries the required attribute as well.
If you are creating your custom provider within the Enterprise Library Visual Studio solution, within the BackingStores folder of the Caching Application Block section, you can use the existing namespace for your class as shown here:
namespace Microsoft.Practices.EnterpriseLibrary.Caching.BackingStoreImplementations
{
[ConfigurationElementType(typeof(CustomCacheStorageData))]
public class MyCustomBackingStore : BaseBackingStore
{
// name of the name/value pair declared in the application
// configuration file <backingStores> section
private const String filePathConfigurationName = "path";
// file extensions for the cached object and cache information files
private const String dataExtension = ".cachedata";
private const String infoExtension = ".cacheinfo";
// internal variable to hold path for the cache files
private String filePath = String.Empty;
....
The remainder of the code in the listing above declares the name for the one configuration value required in the configuration file for this provider - the path="..." attribute. It also declares the file extensions used for the two files for each cached item, and a variable to hold the configured file path value.
You must provide a suitable constructor for your provider class with a signature that matches the way values from the application's configuration file are passed to the provider. If your provider does not include custom design-time configuration support (as in this article), values from the application configuration file appear in a NameValueCollection passed to the constructor when the underlying ObjectBuilder utility instantiates the provider class.
The custom caching provider described here takes a NameValueCollection containing a single configuration value that defines the full path to the folder where the cache files will be created. So, the required signature for the constructor is as shown in this listing:
public MyCustomBackingStore(NameValueCollection configAttributes)
{
// get path to disk file passed in NameValueCollection
String pathAttribute = configAttributes[filePathConfigurationName];
if (pathAttribute != String.Empty)
{
// save the file path
filePath = pathAttribute;
}
else
{
throw new Exception("Error in application configuration, '"
+ filePathConfigurationName + "' attribute not found");
}
}
Inside the constructor, code checks that the configuration file actually does contain the path attribute with a non-empty value, and saves it in the local variable named filePath. By default, as you'll see later, the Configuration Console is not aware of the parameter requirements of a custom provider as so cannot validate them. Therefore, your code must check that all required attributes/parameters are present.
Most of the rest of the operations in the custom provider just consist of file access operations to manipulate the two files that store the details and data for each cached item. The Count property obtains an array of file names for files in the cache file folder that have the file extension specified for data files, and returns the length of the array:
public override int Count
{
get
{
String searchString = String.Concat("*", dataExtension);
String[] cacheFiles = Directory.GetFiles(filePath, searchString,
SearchOption.TopDirectoryOnly);
return cacheFiles.Length;
}
}
Adding a new item to the cache involves creating the two new files required to store it. The Cache Manager passes the hashed storage key value (an integer) and the new CacheItem instance to your method override. The "info" file contains the (String) value of the key, the last access date and time, and the duration of the first "expiration" class in the array of expirations in the CacheItem. Note that (for simplicity in this implementation) the provider requires the CacheItem to use a SlidingTime instance for the first expiration in the array, and only persists this first expiration.
After creating an array containing the "info" values to store, the code creates the information file and writes all the lines in the array to it using the static WriteAllLines method of the File class. If a file with this name already exists, the code deletes it first. As the BaseBackingStore class will call the RemoveOldItem method before calling AddNewItem, there should never be an existing file. However, checking and attempting to delete it will raise an error if the provider cannot update it - for example, if it is read-only or locked:
protected override void AddNewItem(int storageKey, CacheItem newItem)
{
// set up information array values
String[] infoData = new String[3];
infoData[0] = newItem.Key;
infoData[1] = newItem.LastAccessedTime.ToString();
SlidingTime slidingDuration = (SlidingTime)newItem.GetExpirations().GetValue(0);
infoData[2] = slidingDuration.ItemSlidingExpiration.ToString();
// create information file
String infoFile = Path.Combine(filePath, String.Concat(storageKey.ToString(),
infoExtension));
try
{
if (File.Exists(infoFile))
{
File.Delete(infoFile);
}
File.WriteAllLines(infoFile, infoData);
}
catch
{
throw new FileNotFoundException("Cannot create cache info file", infoFile);
}
...
After creating the information file, the provider can serialize the data to cache, and write this to the "data" file using the same file name (the integer hash of the cache key converted to a String). Again, the code attempts to delete any existing file with this name to ensure that a problem with the file will raise an exception to the Cache Manager, which helps to maintain cache synchronization.
Enterprise Library contains many useful features that you can use in your own code, and which reduce the amount of code you have to write. In this case, the Caching Application Block in Enterprise Library already exposes a class named SerializationUtility that can convert an Object into a Byte array, and back again. The sample provider uses the static ToBytes method of this class to serialize the object to be cached, then writes it to the binary disk file using the static WriteAllBytes method of the File class:
...
// serialize object and write to data file
Byte[] itemBytes = SerializationUtility.ToBytes(newItem.Value);
String dataFile = Path.Combine(filePath, String.Concat(storageKey.ToString(),
dataExtension));
try
{
if (File.Exists(dataFile))
{
File.Delete(dataFile);
}
File.WriteAllBytes(dataFile, itemBytes);
}
catch
{
throw new FileNotFoundException("Cannot create cache data file", dataFile);
}
}
To remove a cached item simply means deleting the "info" and "data" files that contain the item. The method code builds the full path to each file as a String, and then calls the static Delete method of the File class for each one:
protected override void Remove(int storageKey)
{
String dataFile = Path.Combine(filePath, String.Concat(storageKey.ToString(),
dataExtension));
String infoFile = Path.Combine(filePath, String.Concat(storageKey.ToString(),
infoExtension));
if (File.Exists(dataFile))
{
// delete files
File.Delete(dataFile);
try
{
File.Delete(infoFile);
}
catch {}
}
else
{
throw new FileNotFoundException("Cannot remove cached item", dataFile);
}
}
Note that it first checks that the "data" file does exist, and throws an exception if not. This is one of the rules for using the IBackingStore interface (or the BaseBackingStore class). The provider must raise an exception, not only if it fails to remove the item from the backing store, but also if the item is not there - it should be; unless the in-memory cache and backing store have become unsynchronized.
The BaseBackingStore class calls the RemoveOldItem method before adding a new item with an existing key to the cache, or if an exception occurs when adding a new item to the cache. Effectively this ensures that updates to the cached items succeed, failed updates are removed, and all errors raise exceptions to the Cache Manager so that it can maintain synchronization of the in-memory cache and the backing store. The rule for the RemoveOldItem method is that it must not raise an exception if the item specified in the call to this method does not exist:
protected override void RemoveOldItem(int storageKey)
{
String dataFile = Path.Combine(filePath, String.Concat(storageKey.ToString(),
dataExtension));
String infoFile = Path.Combine(filePath, String.Concat(storageKey.ToString(),
infoExtension));
try
{
// delete files
File.Delete(dataFile);
}
catch { }
try
{
File.Delete(infoFile);
}
catch {}
}
Each time client code accesses a cached item that carries a SlidingTime expiration, the Cache Manager calls the UpdateLastAccessedTime method of the backing store provider to update the date and time that the item was last accessed. This value is stored in the LastAccessedTime property of the CacheItem class, but the method override just receives a DateTime instance containing the value to set into the cached item.
In the example provider, the code in the UpdateLastAccessedTime method reads the lines from the "info" file as array of String values using the static ReadAllLines method of the File class, updates the appropriate value in the array with the new DateTime value (as a String), deletes the existing "info" file, and creates a new "info" file using the static WriteAllLines method of the File class:
protected override void UpdateLastAccessedTime(int storageKey, DateTime timestamp)
{
String infoFile = Path.Combine(filePath, String.Concat(storageKey.ToString(),
infoExtension));
if (File.Exists(infoFile))
{
String[] infoData = File.ReadAllLines(infoFile);
infoData[1] = timestamp.ToString();
File.Delete(infoFile);
File.WriteAllLines(infoFile, infoData);
}
else
{
throw new FileNotFoundException("Cannot find cache info file", infoFile);
}
}
To remove all cached items when the Cache Manager calls the Flush method is simply a matter of deleting all the "info" and "data" disk files in the cache folder. The code obtains an array of file names for files with the "data" extension, and iterates through the array. For each name, it attempts to delete the "data" and "info" files with that name:
public override void Flush()
{
String searchString = String.Concat("*", dataExtension);
String[] cacheFiles = Directory.GetFiles(filePath, searchString,
SearchOption.TopDirectoryOnly);
foreach (String cacheFile in cacheFiles)
{
String dataFile = Path.Combine(filePath, cacheFile);
String infoName = String.Concat(Path.GetFileNameWithoutExtension(cacheFile),
infoExtension);
String infoFile = Path.Combine(filePath, infoName);
try
{
// delete files
File.Delete(dataFile);
}
catch { }
try
{
File.Delete(infoFile);
}
catch { }
}
}
When an application that uses the Caching Application Block starts, it creates and populates the in-memory cache from the configured backing store. The Cache Manager calls the LoadDataFromStore method in the provider, which must create, populate, and return a HashTable containing all the cached items (without attempting to filter out any that have expired). The key for each item is the integer hash of the cache key, and the value is a CacheItem instance.
The example provider obtains an array of file names for files with the "data" extension, and iterates through the array. For each name, it reads the "info" file and extracts the String cache key name, uses the last access time to generate a DateTime instance, and uses the sliding duration value stored in the third line to create a TimeSpan instance.
Next, the code reads the data for the cached item as an array of bytes using the static ReadAllBytes method of the File class, and then calls the static ToObject method of the SerializationUtility class to recreate the cached object:
protected override System.Collections.Hashtable LoadDataFromStore()
{
Hashtable cacheItems = new Hashtable();
// get a list of cache files
String searchString = String.Concat("*", dataExtension);
String[] cacheFiles = Directory.GetFiles(filePath, searchString,
SearchOption.TopDirectoryOnly);
foreach (String cacheFile in cacheFiles)
{
// read from "info" file
// does not support callbacks or priorities - uses standard values
String infoName = String.Concat(Path.GetFileNameWithoutExtension(cacheFile),
infoExtension);
String infoPath = Path.Combine(filePath, infoName);
String[] infoData = File.ReadAllLines(infoPath);
String itemKey = infoData[0];
DateTime lastAccessed = DateTime.Parse(infoData[1]);
TimeSpan slidingDuration = TimeSpan.Parse(infoData[2]);
// deserialize object from "data" file
Byte[] itemBytes = File.ReadAllBytes(Path.Combine(filePath, cacheFile));
Object itemValue = SerializationUtility.ToObject(itemBytes);
...
Now the code can recreate the original CacheItem instance using its constructor. For simplicity, it assumes a value of Normal for the cache priority, and creates a new SlidingTime expiration instance using the TimeSpan obtained from the "info" file. Finally, it adds the CacheItem to the HashTable, using the hashed integer cache key as the HashTable key, and moves to the next item in the array of cache file names. Once it has added all the cached items, the method returns the HashTable to the Cache Manager:
...
// create CacheItem and add to Hashtable
CacheItem item = new CacheItem(lastAccessed, itemKey, itemValue,
CacheItemPriority.Normal,
null, new SlidingTime(slidingDuration));
cacheItems.Add(itemKey, item);
}
return cacheItems;
}
After you create a custom provider, you must compile it and deploy the assembly to the appropriate location. If you created the provider within the Enterprise Library solution (on your Start menu) in Visual Studio, it will be compiled by default into the assembly Microsoft.Practices.EnterpriseLibrary.Caching.dll that contains the Caching Application Block. The easiest way to achieve this is to use the two utilities available from the Enterprise Library section of your Start menu - Build Enterprise Library and Copy Assemblies to bin Folder. This incorporates the provider into Enterprise Library, and it will appear in the list of installable providers in the Configuration Console.
If you created it in a separate project, you can compile it into a separate assembly, then copy the assembly into the correct location for use in your applications. The best choice for the provider is %Program Files%\Microsoft Enterprise Library January 2006\bin, though you can place it into the bin folder of your application if you wish - where it will be private to and only available within that application. In the Configuration Console, you can load the assembly and select the provider when configuring the Caching Application Block.
To use this custom provider in your application, you must first configure the Caching Application Block to treat it as a custom backing store provider, and specify the configuration information the provider requires.
The example provider you have created inherits from the class CustomCacheStorageData, which is one of the types supported by the Enterprise Library Configuration Console for the Caching Application Block. After adding the Caching Application Block to your application's configuration, right-click the Cache Manager node and select New, then select Custom Cache Storage as shown in Figure 4.

Figure 4 - Adding a Custom Cache Storage item to the application configuration
The Configuration Console adds the Cache Storage node, and the right-hand window displays the properties of the node. You can edit the name, and you must specify the actual object type that implements your custom cache backing store provider. Select the Type property entry and click the (...) button that appears. This opens the Type Selector dialog, where you can select a class that follows the rules of implementing the IBackingStore interface and having a configuration attribute that indicates it is of type CustomCacheStorageData.
If you compiled the provider into the Caching Application Block, using the Visual Studio Enterprise Library solution, your custom provider will show in the Type Selector dialog and you can select it and click OK (see Figure 5). If you compiled it into a separate assembly, click the Load button, navigate to the folder containing the assembly, and select it. The Type Selector dialog will then show your custom provider so that you can select it and click OK.

Figure 5 - Selecting the custom backing store provider in the Configuration Console
The final stage is to configure the remaining properties of the provider. Recall from the earlier discussion (in the section "Creating the Class Constructor") that the Enterprise Library configuration system will expose the attributes from the configuration file as name/value pairs within a NameValueCollection instance passed to the constructor. In the Configuration Console, you specify these name/value pairs as the Attributes property.
Select the Attributes entry in the right-hand window of the Configuration Console and click the (...) button that appears. This opens the EditableKeyValue Collection Editor dialog. In this dialog, click the Add button and enter the key (name) and value for the path attribute the custom provider requires (see Figure 6). Specify a folder that ASP.NET can write to if you are using the provider in an ASP.NET application. Then click OK, and save the configuration to the application's Web.config or App.config file.

Figure 6 - Specifying the name/value pairs for the custom cache backing store provider
If you then open the configuration file in a text editor, you can see the settings for the custom provider.
<cachingConfiguration defaultCacheManager="Cache Manager">
<cacheManagers>
...
</cacheManagers>
<backingStores>
<add path="C:\Temp\" encryptionProviderName=""
type="Microsoft.Practices.EnterpriseLibrary.Caching
.BackingStoreImplementations.MyCustomBackingStore,
Microsoft.Practices.EnterpriseLibrary.Caching,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"
name="Custom Cache Storage" />
</backingStores>
</cachingConfiguration>
Notice the four attributes of the custom provider <add> element, which indicate the property settings for the provider. The type and name attributes correspond to the Type and Name properties, and the NameValueCollection exposes the path attribute and its value. You can configure an encryption provider for a custom cache backing store provider, but as the example provider does not use one, this attribute is empty.
To demonstrate use of the custom provider, the example application contains an option to use either the Isolated Storage backing store provider or the custom backing store provider discussed in this article. If you select the Custom Disk File Cache option and click the button to cache a DataSet, you see the page reports that one item is cached - just as it does for the Isolated Storage provider (see Figure 7).

Figure 7 - Caching a DataSet with the custom cache backing store provider
If you now open Windows Explorer on the folder configured as the path attribute, where the provider creates its cache data files, you will see two files named with the hashed cache storage key, and with the file extensions .cachedata and .cacheinfo (see Figure 8). The .cachedata file contains the serialized representation of the DataSet. The .cacheinfo file contains the (String) cache key name, the last accessed date and time, and the sliding expiration value:
SalesDataset
16/10/2006 12:09:39
00:00:30

Figure 8 - The two files for the cached DataSet in Windows Explorer
Now click the button to load the DataSet back from the cache, and you see it displayed in the page just as with the Isolated Storage provider (see Figure 9). You can close the browser and then reopen it to see that the cache survives application restarts. However, notice that - when the cached item expires after 30 seconds - the Cache Manager calls the methods of the provider to remove the item from the cache, which delete the files.

Figure 9 - Retrieving the cached DataSet and displaying it in the page.
The changes required to the application code to accommodate the custom provider are minimal. If you compiled the provider into a separate assembly, outside of the Caching Application Block, you must add a reference to it to your project and to your code. If you compiled it into the Caching Application Block, existing references to Microsoft.Practices.EnterpriseLibrary.Caching and Microsoft.Practices.EnterpriseLibrary.Caching.Expirations are sufficient.
The code in the example ASP.NET application that creates the DataSet and caches it uses the value of the option buttons to determine which Cache Manager to instantiate (using the static GetCacheManager method of the CacheFactory class) - either the one that uses the custom provider or the one that uses the Isolated Storage provider:
' create DataSet using Data Access Application Block here ...
' ...
' use the Caching Application Block
Dim diskCache As CacheManager
If optCustomCache.Checked Then
' use custom cache provider
diskCache = CacheFactory.GetCacheManager("Custom Cache Manager")
Else
' use default (Isolated Storage) cache provider
diskCache = CacheFactory.GetCacheManager()
End If
...
The code to cache the DataSet is the same whichever provider you choose - indicating how the provider architecture disconnects the Caching Application Block from the physical backing stores, and allows you to extend the block and reconfigure your applications as your requirements change:
...
' store the Dataset in the cache
diskCache.Add("SalesDataset", ds, CacheItemPriority.Normal, Nothing, _
New SlidingTime(TimeSpan.FromSeconds(30)))
lblCount.Text = String.Format("Cache contains {0} item(s)<br />", diskCache.Count)
To extract the DataSet from the Cache Manager is also easy, irrespective of the chosen backing store provider. The code instantiates the selected provider type, and calls the GetData method of that Cache Manager to get the cached item back as an Object type. It can then convert the Object to a DataSet, and display it in a GridView control on the page:
' use the Caching Application Block
Dim diskCache As CacheManager
If optCustomCache.Checked Then
' use custom cache provider
diskCache = CacheFactory.GetCacheManager("Custom Cache Manager")
Else
' use default (Isolated Storage) cache provider
diskCache = CacheFactory.GetCacheManager()
End If
' retrieve the Dataset from the cache
Dim ds As DataSet = DirectCast(diskCache.GetData("SalesDataset"), DataSet)
If ds Is Nothing Then
lblError.Text = "Dataset not found in Cache"
Else
' populate the GridView
GridView1.DataSource = ds.Tables(0)
GridView1.DataBind()
End If
This article demonstrates how easily you can create custom providers that extend the Enterprise Library application blocks. The example shows a custom cache backing store provider, but the principles are the same for all of the application blocks. By inheriting from a suitable base class for the provider, or implementing the appropriate interface, you can connect an application block to the resources or data it processes or uses in such a way that the user can reconfigure their applications as requirements change without having to change their application code.
The example provider is intentionally simple, and so does not implement all the possible features, in order to make it easier to see the Enterprise Library plug-in architecture and provider integration model at work. You can, of course, implement much more complex providers, though you should bear in mind the suitability and considerations discussed in this article.
The one area where the example provider is also deficient in relation to the built-in providers is that it must be configured as a custom provider, and therefore requires users to specify provider configuration values (such as the path for the cache disk files) using name/value pairs. In a related article (see http://www.daveandal.net/articles/EntLibASPNET/), you will see how you can add configuration design support to a provider so that it appears as a first-class member of Enterprise Library and is indistinguishable from the built-in providers.
· Go to Part 5 - Adding Configuration Design Support to a Custom Provider
©2006 Alexander Homer – alex@stonebroom.com