devxlogo

Creating Custom Providers for Enterprise Library

Creating Custom Providers for Enterprise Library

ne of the most useful features of the Microsoft Enterprise Library is its adaptability; all the application blocks are fully extensible. Like ASP.NET itself, Enterprise Library implements a pluggable architecture that lets you 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.

About The Caching Application Block
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 onto a more permanent medium.

?
Figure 1. Pluggable Architecture: The Enterprise Library architecture uses replaceable (pluggable) providers.
?
Figure 2. Pluggable Architecture Details: The Caching Application Block uses a series of separate classes to cache and retrieve data.

The backing store provider is an example of pluggable code. The Caching Application Block ships with three different 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.

My previous articles about using Enterprise Library in ASP.NET applications showed how to use the Isolated Storage provider?mainly because this is the easiest to configure and suffices to demonstrate the Caching Application Block interface. While Isolated Storage is a good choice for many desktop applications, it’s definitely not an ideal technique for ASP.NET server-based applications. In ASP.NET, all anonymous users execute the application under the same account, so Isolated Storage has no way to differentiate between users. In addition, data encryption and the storage location make Isolated Storage data difficult to share across multiple servers in scenarios such as a Web farm.

One way around 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’s where pluggable architecture enters the picture. If the shipped backing store providers don’t meet your needs, you can create a custom backing store provider that persists the data in exactly the way that best suits your requirements.

Designing a Custom Provider
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 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, because doing so 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.

Deciding Where to Start
All the application blocks define an interface for the providers they use, and many contain a base class from which you can inherit from when creating a custom provider. The Caching Application Block defines the IBackingStore interface, which contains the members shown in Table 1.

Table 1. IBackingStore Interface Members: The table shows the IBackingStore Interface members and a description of each.

MemberDescription
CountA read-only integer property that returns the number of objects in the backing store.
AddThis 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.
RemoveThis method takes as a parameter the (String) key of an existing item, and removes that item from the backing store.
UpdateLastAccessedTimeThis method takes two parameters: the (String) key of an item and a DateTime instance and updates the last accessed time property of that cached item.
FlushThis method, which takes no parameters, flushes all stored items from the cache.
LoadThis method, which takes no parameters, returns a HashTable containing all the items from the backing store.

?
Figure 3. Component Interaction: The figure shows how the code components in the Caching Application Block interact.

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 overridden 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.

As you can see from Figure 3, the BaseBackingStore class exposes abstract methods that you must override in your provider. You must implement one property and six methods in your custom backing store provider when inheriting from BaseBackingStore. Here are the property and method definitions:

   // 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 override the Dispose methods of the BaseBackingStore.

Author’s Note: You reference individual cache items using the integer storage key value, which is the hash code of the string key value the user provides for the cache item. The Caching Application Block converts between the string and integer values for you, and all operations within your provider use the integer storage key. The cache item includes a field containing the string key value, as you will see later in this article.

Deciding Where and How to Cache the 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. Therefore, in addition to 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 your extended application block uses. For the Caching Application Block, this means deciding where and how to store the cached data.

A common requirement for ASP.NET applications is caching 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 for some specific storage format or technology. In theory, you can cache the data anywhere, using any format that you wish. But bear in mind that you must provide a robust mechanism that meets the aims of the block and 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 the data 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 reads the persisted data only when the application starts, not every time the application reads data (because it reads it from the in-memory cache), so being able to persist it efficiently is more important than the 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 reads only from its in-memory cache (and not from 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.

Custom File Caching Provider Design
The custom provider described here is simplified so 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.

When the Caching Application Block calls the methods of this example provider to cache data, it will create two disk files in a folder specified in the application’s 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 (again, cache-key is the integer cache key value as a string).

Each cached value uses 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).

Building the Custom File Cache Provider
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.

Referencing Enterprise Library Assemblies and Namespaces
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 2006in 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;

With those namespace references in place, you’re ready to write the custom provider class code.

Creating the Provider 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 following code example shows how the class carries the required attribute as well.

   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  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;          ...

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 above.

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 for each cached item’s two files, and a variable to hold the configured file path value.

Creating the Class Constructor
You must provide a suitable constructor for your provider class with a signature that matches the way ASP.NET passes values from the application’s configuration file to a 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.

Author’s Note: If you implement a specific configuration class for your provider, so that it behaves like the standard providers in the Caching Application Block, values from the configuration file appear as individual parameters to the constructor. I’ll show you how the Enterprise Library Configuration Console supports configuration of custom caching providers in a future article.

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. Here’s the required constructor signature:

   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 preceding constructor, the code ensures that the configuration file contains the path attribute with a non-empty value, and saves it in the local variable named filePath. By default, 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.

Implementing the Count Property
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;       }   }

Implementing the AddNewItem Method
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.

Author’s Note: For simplicity in this implementation the sample provider requires the CacheItem to use a SlidingTime instance for the first expiration in the array, and persists only 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. Because the BaseBackingStore class calls the RemoveOldItem method before calling AddNewItem, there should never be a preexisting file. However, any attempt to delete a locked or read-only file will raise an error:

   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 must serialize the data to cache, and write it 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 that 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);     }   }
Author’s Note: If you need to support multiple expiration types and different cache priorities, you must adapt the AddNewItem method implementation to store details of the expiration types and the cache priority, and their values, in the “information” file.

Implementing the Remove Method
To remove a cached item simply involves deleting the “info” and “data” files that contain the item. The Remove 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 whether the “data” file exists, and throws an exception if it does 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 desynchronized.

Implementing the RemoveOldItem Method
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 {}   }

Implementing the UpdateLastAccessedTime Method
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 File.ReadAllLines method, 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 File.WriteAllLines method:

   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);     }   }

Implementing the Flush Method
Removing all the 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 { }     }   }

Implementing the LoadDataFromStore Method
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 File.ReadAllBytes method, and then calls the static SerializationUtility.ToObject method 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. After adding 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;   }
Author’s Note: If you need to support multiple expiration types and different cache priorities, you must adapt the LoadDataFromStore method implementation to retrieve details of the expiration types and the cache priority, and their values from the “information” file. Then you can convert them into the appropriate object types and build the corresponding CacheItem instance.

Compiling and Deploying a Custom Provider
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.” These actions incorporate the provider into Enterprise Library, and it will appear in the list of installable providers in the Configuration Console.

Alternatively, if you created your custom provider in a separate project, you can compile it into a separate assembly, and 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 2006in?although 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.

?
Figure 4. Custom Cache Storage: The figure shows the process for adding a Custom Cache Storage item to the application configuration.

Using the Custom File Cache Provider
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.

Configuring the Custom File Cache Provider
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.

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 has a configuration attribute that indicates it is of type CustomCacheStorageData.

?
Figure 5. Selecting a Custom Backing Store Provider: Select your custom backing store provider from the Type Selector dialog in the Configuration Console.

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 simply 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.

The final step 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 to save the configuration.

?
Figure 6. Custom Cache Backing Store Configuration: In the EditableKeyValue Collection editor, specify the name/value pairs for the custom cache backing store provider.
Author’s Note: For an ASP.NET application, you should save the file as Web.config. In a Windows Forms or Console application, you should save the file as App.config. If your ASP.NET application already has a Web.config file, as is the case when you use Visual Studio or Visual Web Developer to create a new Web site, you can open that file into the Enterprise Library Configuration Console, edit it to add and configure the blocks you need, then save it. This does not change any other settings in the file.

If you then open the configuration file in a text editor, you’ll see the settings for the custom provider:

               ...                         

Notice the four attributes of the custom provider 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 in this example.

Author’s Note: To implement encryption in your custom provider, you can use the Configuration Console to add a new Symmetric Storage Encryption node to the Cache Storage node configuration, and configure an encryption provider from the Cryptography Application Block for this node. The name of the encryption provider then appears as the StorageEncryption property of the custom provider, and your code can encrypt and decrypt the data in the same way as the example application shown in earlier related articles.

Working with the Custom File Cache Provider in ASP.NET
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 (see Figure 7).

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, 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 7. Cache Testing: The sample application lets you test the custom cache backing store provider by caching a DataSet.
?
Figure 8. Cached Files: In Windows Explorer, you can see the two files created to cache the DataSet.

Now click the button to load the DataSet back from the cache, and you see it displayed in the page (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 deletes the files.

?
Figure 9. Retrieving Cached Data: After retrieving the cached DataSet, the sample application displays it on the page.

The changes required to application code to accommodate a 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. Note that the application code is in VB.NET even though the Enterprise Library itself?and the custom provider code you’ve seen here?are both in C#:

   ' 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)
", diskCache.Count)

Extracting 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 Cache Manager’s GetData method 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 pluggable 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 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 users can reconfigure their applications as requirements change without having to modify their application code.

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.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist