devxlogo

Caching Data with a Web Service in Enterprise Library

Caching Data with a Web Service in Enterprise Library

ometimes you find yourself thinking, “I wonder if I can make my application do this…?” The target of your thoughts might not immediately seem to be a useful, or even sensible, feature; but most developers tend to be attracted to unusual problems that offer a challenge.

For me, that process started when I was working with the Caching Application Block in Enterprise Library 2.0, trying to demonstrate how easy it is to create your own custom providers. I work mainly in ASP.NET, where the requirements for caching differ from most Windows Forms applications. For example, using the default Isolated Storage provider is probably not a realistic option in a web site, and even less so in a multiple-server web farm.

An earlier article of mine described the process for interfacing a custom provider with the Caching Application Block and adding support for the Enterprise Library configuration tools. The custom provider described in that article simply writes the cached data to disk in a configurable folder. This provides opportunities for caching in a way more suited to ASP.NET, although it still does not provide a truly “shared” caching mechanism due to the way that the Caching Application Block works internally.

To achieve a flexible and shared caching mechanism, you need a central cache store. One possible approach is to use the Database Caching provider supplied with Enterprise Library. However, all web caching approaches face the problem of connectivity between the application and the cache repository. Another approach is to use a web service. As it wasn’t obvious whether that would even work, let alone be fast enough to be useful, the approach warranted a full test implementation.

Why Use a Web Service?
The Caching Application Block’s provider mechanism lets you create a custom provider that stores cached data anywhere you want. It was this that made me wonder if it was possible to cache data within or through a web service, which would allow the provider to cache its data almost anywhere?remotely or locally?without having to write specific code that is directly integrated within Enterprise Library.

The principle is simple enough. Instead of having the backing store provider within the Caching Application Block interact directly with the backing store (the usual approach, as implemented in the Isolated Storage provider and Database provider), the backing store provider simply packages up the data and sends it to a web service.

The web service can then cache or manipulate the data in any way you need. And, if the backing store provider is sufficiently configurable, you can change the URL of the target web service any time you like. In addition, you can add more than one Cache Manager and backing store provider to an application, allowing it to cache the data through multiple web services. Finally, adding support for “partitions” within the provider means that you can implement multiple separate caches within the target backing store.

The Design of the Web Service Caching Provider
Figure 1 shows a high-level view of the approach. As you can see, the core principle is simple enough, although the implementation proved somewhat trickier than first expected. However, while having some limitations, the result does provide the features I initially wanted to achieve.

?
Figure 1. Web Service Caching Provider: The figure shows a high-level view of the web service Caching Provider and associated mechanism for the Caching Application Block.

Issues that you may want to visit are:

  • Transmitted Data Format?I chose to simplify the types and capabilities somewhat to allow the use of the widest possible range of target web services (including non .NET platforms).
  • Cache Duration Expiration Types?The example described here supports only the SlidingTime expiration type.
  • Expiration Callbacks?You would only need this if you intend to use the mechanism within a Windows Forms application?this feature is not really useful in ASP.NET applications.
  • Combining Local Caching with Remote Caching through the Web Service?This could improve performance and reduce network load, for example by using the remote service only for archiving data.

Despite the limitations, remote caching does work reasonably well with small or medium sized data items, over a reasonably fast network link. The Caching Application Block uses an in-memory cache to provide fast local performance when reading cached data, and only uses the backing store provider to implement a persistent store. Therefore, the only interactions with the backing store are:

  • Loading the cached items when the application starts up
  • Adding new items to the cache when the user adds them to the in-memory store
  • Removing existing items from the cache when the user removes them from the in-memory store
  • Flushing the cache to remove all items
  • Providing a count of the number of cached items

As you can see from the list, large volumes of data move over the wire to the web service only at application startup. In contrast, adding an item to the cache involves moving only the data for that item over the wire, and only one way: into the backing store. Other operations just send or receive small SOAP packets that include items such as the cache key, an updated “last accessed time,” or an integer value.

Cache Persistence and Partitioning
The web service example provided with this article stores cached data as disk files. By default, it uses a local folder, though?providing the account you run the web service under has the relevant permissions?there is no reason why the cache root could not be on a mapped or remote drive. Another possibility is to use the web service to store the cached data in a database, or in any other persistent storage you require.

You may even decide to implement a separate component, business logic layer, or data tier to handle the actual storage. In this way, the web service is simply a communication conduit that interfaces the Caching Application Block with a local or remote backing store of your choosing.

The sample provider stores the cached data as disk files (using the same approach as my previous article), placing them in a subfolder under the root caching folder. However, one interesting feature (see Figure 1) of the sample application is the ability to define a partition or filter value that the web service can use to implement multiple separate caches without requiring you to implement multiple web services. In the configuration of the web service Cache Provider, you can specify a name for the partition that this provider will use.

The partition name gets sent to the web service with every call from the Caching Application Block. The web service can use the partition name to store cached items in different folders on disk, in different database tables, or under different keys?depending on how you choose to implement persistent storage. The sample web service stores cached data in subfolders that correspond to the partition name.

Creating a Web Proxy Class for the Caching Provider
The first?and in fact the trickiest?issue in creating the provider turned out to be implementation of a suitable web service proxy. Initially, I wanted to implement a system where users could create a custom proxy and install it with the web service backing store provider simply by specifying the proxy class type within the Enterprise Library configuration.

However, this approach turned out to be difficult, made even more so because the backing store provider would need to instantiate the proxy without previously knowing the type. While you can instantiate the proxy in that way using the Activator.CreateInstance method, figuring out how to integrate varying classes, types, and properties from a custom proxy class seemed unduly complicated.

Generating a Web Proxy Class Dynamically
One approach to creating the proxy would be to generate it dynamically from the web service Description Language (WSDL) document exposed by the target web site. There are well-known techniques for this; described, for example, in the MSDN article “Calling an Arbitrary Web Service.” Roman Kiss wrote a comprehensive and useful article on this technique as well. You can even get a ready-built component from GotDotNet to generate web services from WSDL.

However, it soon became clear that this approach was also overkill. The interface for the proxy and the corresponding interface of the web service are fixed by the requirements of a Caching Application Block backing store provider. So there’s no need to generate the proxy dynamically; the WSDL would be the same every time.

User-defined or Built-In Proxy?
Other issues are determining how much variability and configurability should be available for the target web service, and making it configurable within the backing store provider, as these affect the proxy implementation. For example, should the configuration support user-defined namespaces for the web service methods? Or should it be possible to specify the namespace and the target URL at runtime?

In the end, after much consideration, it became clear that?except for the target web service URL?the proxy does not actually need to be configurable by users. Hard-coding the namespace binding into the proxy is not unreasonable, because users can specify the required namespace binding within their custom web service classes.

Finally, because the interface is fixed (based on the methods that a custom backing store must implement), it seemed easier to create a public Interface class that describes the interface. Visual Studio (VS) can then insert stub code for the interface into the web service automatically.

Using the Interface class means the proxy can become a permanent class compiled into Enterprise Library. Users then need only build the web service by implementing the same interface and using the specified namespace binding.

Author’s note: One reason for not offering configurable namespace binding is that I have yet to find a way to dynamically create the WebServiceBindingAttribute required within the proxy class, or change its properties at runtime. The same applies to the SoapDocumentMethodAttribute on each proxy method. Comprehensive searches of the web have not revealed any solution, so perhaps it is not possible.

?
Figure 2. Web Service-Based Caching: Here’s the overall design and components for the sample web service-based caching mechanism.

Figure 2 shows the final design of the web service-based caching mechanism in the sample application. You can see that the custom backing store provider, its configuration data storage and design support classes, the proxy class, and its interface are all compiled within the Caching Application Block.

The Caching Web Service class used in the example persists cached data directly (annotated as “A” in Figure 2). However, there is no reason why it could not communicate with other components or services (annotated as “B” in Figure 2), and use them to cache the data as required.

Building the Web Proxy Class
The easiest way to create a web service proxy class is to use the WSDL tool provided with Visual Studio. You may need to extract wsdl.exe from your VS 2005 setup files, as it is not installed automatically with the standard installation. You need the two files wsdl.exe and wsdl.exe.config (The WSDL tool is a command-line executable and has no GUI).

To create a proxy class with wsdl.exe, you must provide an XML schema or a WSDL document as the source. Here’s one process:

  1. Design the interface required for communication between the backing store provider and the web service.
  2. Create an Interface class that defines the communication interface.
  3. Build a web service containing just the method outlines by implementing this interface within a new ASP.NET Web Service project in Visual Studio 2005.
  4. Specify the namespace binding you want to use for the proxy and web service in the WebService attribute of the outline web service.
  5. Install the outline web service on a local web server.
  6. Execute wsdl.exe, specifying the URL of the outline web service to generate the required proxy class.
  7. Copy the proxy class into the Caching folder of the Enterprise Library source files (created, by default, in the EntLibSrc folder when you installed Enterprise Library. Rerun the Enterprise Library setup if you did not install them initially).

Designing the Web Service Interface
The interface required for communication between the backing store provider and the web service is basically the set of methods that the backing store provider must implement, such as LoadDataFromStore, Remove, AddNewItem, and Count (implemented as CachedItemCount). I modified the signatures to include the name of the cache partition, and chose to change the types to simplify transmission over the wire by using mainly Base64-encoded String and String Array types. However, this is an arbitrary decision; you can choose the types that best suit your requirements.

The following code shows the Interface class (named ICustomCacheWebService) that defines the communication interface:

   using System;   using System.Collections;      namespace Microsoft.Practices.EnterpriseLibrary.Caching   {      public interface ICustomCacheWebService      {         Int32 CachedItemCount(String partitionName);         String AddNewItem(String partitionName,             String storageKey, String[] cachedItemInfo,             String base64ItemBytes);         String Remove(String partitionName, String storageKey);         String RemoveOldItem(String partitionName,             String storageKey);         String UpdateLastAccessedTime(String partitionName,             String storageKey, DateTime timestamp);         String Flush(String partitionName);         String[] LoadDataFromStore(String partitionName);         // String array:         // @ Index + 0 = cache key         // @ Index + 1 = last accessed date/time         // @ Index + 2 = duration (seconds)         // @ Index + 3 = Base64 encoded data         // repeats in multiples of 4      }   } 

Implementing the Interface in a Web Service
After creating the interface class, copy it into the App_Code folder of a new C# Web Service project in Visual Studio.

Author’s Note: you must use the same language to create both the interface and the web service.

Then add the interface to the class declaration (you may need to include the full namespace-qualified names):

   public class Service : WebService, ICustomCacheWebService

Now right-click the interface name and select Implement Interface, and then click Implement Interface in the fly-out menu. This creates all the outline methods required for the web service and the web service proxy.

Author’s Note: If you click the option “Implement interface explicitly” instead, Visual Studio uses the full namespace-qualified names for each member instead of just the member name.

Next, change the value of the Namespace property of the WebService attribute within the new web service from its default (http://tempuri.org/) to the namespace binding you want to use for communication between your backing store provider and the target web service(s):

   [WebService(Namespace =       "uri:mycacheservice/somequalifier/samples")]

Running WSDL to Generate the Proxy Class
After the outline web service is available, open a Command window and execute wsdl.exe to generate the required proxy class. To see all the options for WSDL, type:

   wsdl.exe /?

If you are happy with the default language for the proxy class (C#), all you need enter to generate the class is a command using this syntax:

   wsdl.exe http://path-to-your-web-service/      your-web-service-file-name.asmx

Insert the /language:VB option before the URL if you want the proxy class generated in Visual Basic.NET instead of C#, and include the /out:file-path-and-name option if you want to generate the class file with a specific name, or in a specific folder instead of the same folder as the WSDL tool. The sample proxy provided with the samples is named CustomCacheWebServiceProxy.

Modifying the Generated Proxy Class
The generated proxy class will contain the methods exposed by the web service. As an example, here’s a method that takes the partition name and the storage key of a cached item:

   public string Remove(string partitionName, string storageKey)

However, you must make a few minor changes to the class before you can use it. By default, the WSDL tool does not add a namespace to the proxy class (although you can specify one as an option if you wish). However, if you want to locate the proxy class within a namespace in your modified version of the Enterprise Library Caching Application Block, you can simply add it to the class. This is the namespace used by the Caching Application Block:

   namespace Microsoft.Practices.EnterpriseLibrary.Caching   {     ... WSDL generated code here ...   }

Next, add the name of the interface for the custom class and web service so that you can access the proxy in your code using the interface type instead of requiring the proxy class type:

   public partial class CustomCacheWebServiceProxy       : System.Web.Services.Protocols.SoapHttpClientProtocol,       ICustomCacheWebService

To allow users to configure the URL of the proxy to point to their custom Caching Web Service, modify the proxy class constructor to accept the URL as a parameter, and set the Url property of the class to this value instead of the fixed value that WSDL generates within the constructor:

   public CustomCacheWebServiceProxy(String webServiceUrl)    {      this.Url = webServiceUrl;   }

Finally, if you want to use a different namespace binding from that specified in your temporary outline web service, change the value of the namespace in the WebServiceBindingAttribute near the start of the class, and in all of the SoapDocumentMethodAttribute instances on the members of the proxy class.

Implementing the Caching Provider
With the proxy class in place, you can now create the provider for the Caching Application Block. The principles are the same as in the previously referenced article. The techniques for implementing design support for the Enterprise Library configuration tools are also the same.

The only real differences are that the Web Service Caching provider has three custom properties that you can set in the application configuration:

  • The default cache duration (sliding expiration time) that the provider will use if you do not specify the duration when caching an item
  • The name of the partition or filter value to pass to the web service with each cache method call
  • The URL of the target web service

Figure 3 shows how the design classes for the custom provider enable configuration through the Enterprise Library Configuration Console. The DefaultCacheDuration and PartitionName are set automatically to the default values shown in Figure 3, but users can edit them as required.

?
Figure 3. Configuring the Custom Web Service Caching Provider: You can see the three custom properties (in addition to the Name property) that it exposes.

In reality, the backing store provider (CustomWebServiceBackingStore.cs in the CachingBackingStoreImplementations subfolder) has to pass calls only from the Caching Application Block to the proxy class methods. Therefore, the bulk of the work within the methods lies in converting values passed to the provider into the correct types for the proxy class.

The class declaration specifies the configuration element type for the class as an instance of the CustomWebServiceCacheStorageData class (discussed later). Notice that the provider class inherits from BaseBackingStore. Both these features are requirements for a simple custom cache backing store provider:

   [ConfigurationElementType(typeof(      CustomWebServiceCacheStorageData))]   public class CustomWebServiceBackingStore :       BaseBackingStore   {     ...

The Provider Class Constructor
The provider constructor takes the values passed from its configuration data storage class and stores them in local variables so it can pass them to the appropriate proxy methods. Here’s the class constructor and the local variable declarations:

   // name of the name/value pairs declared in the application   // configuration file  section   private const String serviceUrlAttribute = "webServiceUrl";   private const String servicePartitionNameAttribute =       "partitionName";   private const String serviceDefaultDurationAttribute       = "defaultCacheDuration";      // local variables   private ICustomCacheWebService serviceProxy = null;   private String wsUrl = String.Empty;   private String wsPartition = String.Empty;   private int wsDefaultDuration = 0;      public CustomWebServiceBackingStore(String serviceUrl,       String partitionName, int defaultCacheDuration)   {      if (serviceUrl != String.Empty &&          partitionName != String.Empty &&          defaultCacheDuration != 0)      {         wsUrl = serviceUrl;         wsPartition = partitionName;         wsDefaultDuration = defaultCacheDuration;      }      else      {         throw new Exception(            "Error in application configuration");      }   }

Obtaining a Reference to the Proxy Class
Every property and method in the provider must obtain a reference to the proxy to call its methods; therefore, it makes sense to hang on to the reference where possible. This is true even in ASP.NET applications, where each page load instantiates the block and the provider, because the block often calls more than one method as part of a caching operation (for example, it calls the RemoveOldItem method before calling the AddNewItem method).

Therefore, the provider includes a private method that returns a new or existing instance of the proxy class. This instance (as an ICustomCacheWebService type) is stored locally in the variable named serviceProxy:

   // get a new or existing instance of the    // Web Service Proxy object   private ICustomCacheWebService GetWebServiceProxy()   {      if (serviceProxy == null)      {         try         {            serviceProxy = new CustomCacheWebServiceProxy(wsUrl);         }         catch (Exception ex)         {            throw new Exception(               "Cannot create Web Service Proxy", ex);         }      }      return serviceProxy;   }

Calling the Proxy Class Methods
Every method and property must pass the partition name to the proxy so that the target web service can access each item from the correct location. For example, the Flush method obtains a reference to the proxy and then passes the partition name to the Flush method of the proxy:

   public override void Flush()   {      ICustomCacheWebService proxy = GetWebServiceProxy();      String errMessage = String.Empty;      try      {         errMessage = proxy.Flush(wsPartition);      }      catch (Exception ex)      {         throw new Exception(            "Failed to execute Flush method", ex);      }      if (errMessage != String.Empty)      {         throw new Exception(            "Cannot flush Web Service cache partition '"             + wsPartition + "'. Web Service error: " +             errMessage);      }   }

To provide enhanced debugging and error handling capabilities (always a good idea when working with web services), the method catches any exceptions and provides details of what went wrong. If an error occurs, the service passes the error message back to the proxy, and on to the provider, as a String value that the provider can return to the user. If no error occurs, the web service returns an empty string.

Author’s Note: This article shows the C# code for the sample web service methods and the test application, but you can get the VB.NET code by downloading and opening the sample projects.

Converting Data Types for the Web Service
The data types chosen for the sample provider proxy and web service interface are different from those exposed by the Caching Application Block when it calls the backing store provider. Therefore, some of the methods in the provider must perform type conversion. For example, the AddNewItem method must extract the individual values from the CacheItem instance passed to it and match these to the types specified in the proxy interface.

The next listing shows how the AddNewItem method builds a String Array named infoData containing the item key, the “last accessed” date/time, and the sliding time cache duration in seconds:

   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();      // see if there is a cache sliding expiration       // duration specified      ICacheItemExpiration[] cie = newItem.GetExpirations();      if ((cie.Length > 0) && (cie[0] is SlidingTime))      {         // get sliding time value and convert to a string          // for the info array         SlidingTime slidingDuration = (SlidingTime)            newItem.GetExpirations().GetValue(0);         infoData[2] =             slidingDuration.ItemSlidingExpiration.ToString();      }      else      {         // no duration specified, so use the default value         infoData[2] = wsDefaultDuration.ToString();      }      ...
Author’s Note: This simple implementation supports only sliding time durations, though you could extend it to support the other types defined within the Caching Application Block if required.

The code then converts the cached item to a Base64-encoded string. The core Enterprise Library contains a SerializationUtility class that you can use to serialize and de-serialize objects:

      ...      // serialize object and convert to Base64 encoding      Byte[] itemBytes = SerializationUtility.ToBytes(         newItem.Value);      String itemString = Convert.ToBase64String(itemBytes);      ...

Finally, the method gets a reference to the proxy class and calls its AddNewItem method. The code handles any exceptions and generates error messages in the same way as you saw earlier for the Flush method:

      ...      // get proxy instance, call method, and check for errors       ICustomCacheWebService proxy = GetWebServiceProxy();      String errMessage = String.Empty;      try      {         errMessage = proxy.AddNewItem(            wsPartition, storageKey.ToString(),            infoData, itemString);      }      catch (Exception ex)      {         throw new Exception(            "Failed to execute AddNewItem method", ex);      }      if (errMessage != String.Empty)      {         throw new Exception(            "Cannot add new item to cache partition '"             + wsPartition + "'. Web Service error: " +             errMessage);      }   }

Loading Cached Items
When the Caching Application Block initializes, it loads any persisted cache items from the backing store by calling the provider’s LoadDataFromStore method. This is perhaps the most complex method, because it must convert the String Array returned from the web service proxy that contains all the cached items into a Hashtable of CacheItem instances.

The first stage of the method, shown in the next listing, creates a Hashtable, gets a reference to the proxy, and calls its LoadDataFromStore method; specifying as usual the partition name defined for this provider instance. It checks whether any cached items exist, and if not, returns an empty Hashtable.

Note that the LoadFromDataStore method of the web service has to return a String Array of size 1 (instead of a simple String), when an error occurs. The code in the LoadDataFromStore method of the provider checks to see if an error occurred, and returns the details to the user:

   protected override System.Collections.Hashtable LoadDataFromStore()   {      Hashtable cacheItems = new Hashtable();      ICustomCacheWebService proxy = GetWebServiceProxy();      String[] serviceCachedItems =          proxy.LoadDataFromStore(wsPartition);      // see if cache partition is empty      if ((serviceCachedItems == null) ||          (serviceCachedItems.Length == 0))      {         return cacheItems;      }      // see if the Web Service returned an error      if (serviceCachedItems.Length == 1)      {         throw new Exception(            "Cannot load items from cache partition '" +             wsPartition + "'. Web Service error: " +             serviceCachedItems[0]);      }      ...

Provide that no error occurs, the next section of the method code iterates through the String Array, extracting the values for the cached item. Each cached item is stored as four consecutive values in the array, so the code indexes into the array to get each value for a cached item, and then skips four places to the next cached item.

For each cached item, the code extracts the key, the “last accessed” date/time, the sliding duration (in seconds), and the Base64-encoded string containing the cached object or value. The code converts these values to the appropriate types and then generates a new CacheItem instance, adds it to the Hashtable, and continues with the next cached item in the array:

      ...      // read the cached item data into the Hashtable      Int32 itemIndex = 0;      while (itemIndex 

The remaining methods in the provider class?Count (which calls the CachedItemCount method of the proxy), Remove, RemoveOldItem, and UpdateLastAccessedTime?work in much the same way as those you have just seen. Each method passes the partition name, the integer hash of the cache key (called the storage key), and any other required parameters to the proxy class methods. In addition, except for CachedItemCount, the web service returns any error message as a String that the method can use to generate an appropriate error message. Because the CachedItemCount method returns an integer value, the error indicator in this case is -1. The file CustomWebServiceBackingStore.cs in the downloadable code contains all the methods.

Adding Configuration and Design Support for the Provider
The custom Web Service Cache provider requires a configuration data class that exposes the values in the application configuration to the provider. This class, defined in the file CustomWebServiceCacheStorageData.cs (in the CachingConfiguration subfolder), looks and works much like the equivalent for the custom caching provider discussed in previous articles. The only difference is that it exposes the three custom properties for this provider, as well as the provider type and name.

To implement design support for the Enterprise Library configuration tools, you must also:

  • Create a "storage node" class that the configuration tools use to store and display the node for the custom provider.
  • Modify the registrar classes that add nodes and commands to the configuration tools.
  • Add the relevant entries to the resources file for the configuration classes.

The sample project includes a folder named EntLibSourceFiles that contains the files and code required to implement design support, including:

  • CustomWebServiceCacheStorageData.cs. This class stores the run-time settings and data for the provider.
  • CustomWebServiceCacheStorageNode.cs. This class implements the configuration node for the configuration tools and stores the configuration data.
  • CachingNodeMapRegistrar.cs.additions. This file contains the C# code you must add to the CachingNodeMapRegistrar.cs class that registers configuration nodes with the configuration tools.
  • CachingCommandRegistrar.cs.additions. This file contains the C# code you must add to the CachingCommandRegistrar.cs class that registers commands with the configuration tools.
  • Resources.resx.additions. This file contains the entries you must add to the Resources.resx file in the CachingConfigurationDesignProperties folder of Enterprise Library.

To understand how these classes work, and exactly how you add or update them within Enterprise Library, see the articles in the Related Resources section of this article.

With the custom handler, proxy class, and design-support classes complete, it's time to compile the Caching Application Block and copy the assemblies to the required location. The easy way to do that is to run the BuildLibraryAndCopyAssemblies.bat file located in the EntLibSrcApp Blocks subfolder of the Enterprise Library source files. All the final assemblies, and the Configuration Console, reside in the EntLibSrcApp Blocksin subfolder.

Recall that, when using the unsigned version of the Enterprise Library assemblies in the EntLibSrcApp Blocksin folder, you must use the version of the Configuration Console (EntLibConfig.exe) located in this folder to configure your applications. See the sidebar "Editing Enterprise Library 3.x Configuration Files" for more information.

Building the Target Web Service
After updating Enterprise Library as discussed in the preceding sections of this article, the only remaining task is to build the web service that will communicate with the Caching Application Block and persist the cached items. How it does this depends on your own requirements.

The main constraints and requirements for the web service are:

  • It must implement the ICustomCacheWebService interface, and functionally perform in the correct manner by returning an error when a method fails
  • It must reside within the namespace you specified in the proxy class you created within Enterprise Library
  • It must be available to anonymous requests that use the SOAP protocol, unless you implement credential handling and presentation in your custom proxy and backing store provider

The sample web service, implemented by the class SampleCachingWebService in the SampleCacheWebServiceApp_Code subfolder of the examples, fulfills these requirements. To reference the ICustomCacheWebService interface, you must add a reference to the Caching Application Block assembly (or the namespace where you defined the interface) to your project and class:

   // in C#:   using Microsoft.Practices.EnterpriseLibrary.Caching;      ' in Visual Basic.NET:   Imports Microsoft.Practices.EnterpriseLibrary.Caching

You define the namespace of the web service in the WebService attribute that decorates the class, and you should include the WebServiceBinding attribute that specifies conformance with the Basic Profile 1.1 as shown here:

   // in C#:   [WebService(      Namespace="your-proxy-class-namespace")]   [WebServiceBinding(      ConformsTo = WsiProfiles.BasicProfile1_1)]      ' in Visual Basic.NET:    _    _

The class itself will inherit from System.Web.Services.WebService; you add the interface declaration for ICustomCacheWebService here as well:

   // in C#:   public class SampleCachingWebService :       System.Web.Services.WebService, ICustomCacheWebService      ' in Visual Basic.NET:   Public Class SampleCachingWebService      Inherits System.Web.Services.WebService      Implements ICustomCacheWebService

Implementing the Sample Caching Web Service
The remainder of the web service class implementation consists of code to implement the methods defined in the ICustomCacheWebService interface as well as any helper methods or other code you need to persist the cached items. As discussed earlier, the sample web service defines a root cache folder on the hard disk, and generates subfolders within this root folder for each partition name as the user caches data through the custom backing store provider.

Therefore, the code first declares the root folder path and the file extensions it will use to store the cached data. The information about the cached item (the key, "last accessed" date/time, and cache duration in seconds) reside in a text file with an extension of .cacheinfo as a series of String values that can easily be read into or written from the String Array passed to the web service methods. The web service converts the serialized item passed to the web service as a Base64-encoded String to a Byte Array and stores it as a binary file with a .cachedata extension:

   // root folder for cached data files   private const String cacheRootFilePath = @"C:Temp";      // file extensions for the cached object and    // cache information files   private const String dataExtension = ".cachedata";   private const String infoExtension = ".cacheinfo";

Converting, Persisting, and Updating Cached Items
The methods in the sample web service have two main tasks to perform. They must convert the values received from the proxy within the Caching Application Block into the appropriate types for the chosen storage medium, and then persist or manipulate them as required. In fact, the types chosen for exposure by the proxy are well suited to most storage mediums, such as disk files or database tables.

As an example of the methods, the CachedItemCount method uses the partition name to build a path to the cached file location, and uses the Directory.Exists method to check whether that folder exists. If not, it returns zero. If the folder does exist, the code creates a search string to locate all files with the .cachedata extension, and calls the Directory.GetFiles method to get an array of their names. It can then return the length of this array as the number of cached items. If an exception occurs, the code just returns the error indicator value -1:

   [WebMethod(Description="Returns the number of items " +       "in the specified cache partition.")]   public Int32 CachedItemCount(String partitionName)   {      // create partition folder path and check if it exists      String filePath = Path.Combine(cacheRootFilePath,          partitionName);      if (Directory.Exists(filePath))      {          // create file specification to search for cache files         String searchString = String.Concat("*", dataExtension);         try         {            String[] cacheFiles = Directory.GetFiles(               filePath, searchString,               SearchOption.TopDirectoryOnly);            return cacheFiles.Length;         }         catch         {            return -1;  // error indicator         }      }      else      {         // partition folder does not exist, so no cached items         return 0;          }   }

To add a new item to the cache, the AddNewItem method in the web service must first convert the values passed to the method into the correct types. It must check whether the target partition subfolder exists, and create it if not. Then the code creates the paths and names for the "information" and "data" files (the file name is the storage key?a hash of the actual cache key specified by the user):

   [WebMethod(Description=      "Adds a new item to the specified cache partition.")]   public String AddNewItem(string partitionName, string       storageKey, string[] cachedItemInfo, string base64ItemBytes)   {      // create partition folder path and check if it exists      String filePath = Path.Combine(         cacheRootFilePath, partitionName);      if (!Directory.Exists(filePath))      {         try         {            Directory.CreateDirectory(filePath);         }         catch (Exception ex)         {            return "Cannot create partition folder" + ex.Message;         }      }      // create path and file names for information and data files      String infoFile = Path.Combine(filePath,          String.Concat(storageKey.ToString(), infoExtension));      String dataFile = Path.Combine(filePath,         String.Concat(storageKey.ToString(), dataExtension));      ...

The next stage is to write the files to the disk. The "information" file is easy because the File.WriteAllLines method accepts a String Array and writes it as a series of text lines separated by carriage returns. For the "data" file, the code first converts the incoming Byte64-encoded string to a Byte Array, and then calls the File.WriteAllBytes method:

      ...      try      {         // create information file         if (File.Exists(infoFile))         {            File.Delete(infoFile);         }         File.WriteAllLines(infoFile, cachedItemInfo);      }      catch (Exception ex)      {         return "Cannot create information file" + ex.Message;      }      // decode object from Base64 string and write to data file      Byte[] itemBytes =          Convert.FromBase64String(base64ItemBytes);      try      {         // create data file         if (File.Exists(dataFile))         {            File.Delete(dataFile);         }         File.WriteAllBytes(dataFile, itemBytes);         return String.Empty;      }      catch (Exception ex)      {         return "Cannot create data file '" + dataFile +             "'. " + ex.Message;      }   }

Notice that the code first deletes any existing file with the same name (the storage key), and that it creates the .cacheinfo file before the .cachedata file. As the methods to count and retrieve cached items look for the "data" files, an extra .cacheinfo file will not affect the caching mechanism if an error writing the data file leaves an orphaned information file in the cache. Meanwhile, should any errors occur, the method returns a suitable String message to the Caching Application Block proxy.

As you can guess, removing an item from the cache simply involves deleting the appropriate information and data files. To comply with the rules for cache backing store providers, the Remove method reports an error if the specified cache item does not exist, while the RemoveOldItem method does not. The Flush method deletes all files in the specified partition (subfolder).

The UpdateLastAccessedTime method also has a simple task. It takes the DateTime instance passed from the backing store provider, reads the array of information strings from the information file, updates the relevant item in the array, and writes it back to the disk. This is the core section of code for the process:

   // get existing information array and update it   String[] infoData = File.ReadAllLines(infoFile);   infoData[1] = timestamp.ToString();   File.Delete(infoFile);   File.WriteAllLines(infoFile, infoData);

Therefore, even when clients are accessing cached items regularly, the bandwidth and processing requirements of the web service caching mechanism are minimal.

Loading Cached Items
As you saw in the sample backing store provider earlier in this article, the most arduous task that the system must perform is loading all the cached items when the Caching Application Block first initializes. The sample Caching Web Service must read all the information and data files containing cached items from the specified partition subfolder, and generate a String Array in the format documented earlier in this article.

However, LoadDataFromStore is also the first method called by the backing store provider when it initializes, and so the code within this method must create the specified subfolder if it does not already exist (you could separate the code that does this, which is used more than once, into a separate routine if you wish). This check also indicates to the client if there is a problem (such as if the folder is read-only or unavailable, or if the web service account has insufficient permissions) right at the start of the process?before the user attempts to cache any data:

   [WebMethod(Description=      "Returns a Srting array containing all the cached " +       "items for the specified partition.")]   public string[] LoadDataFromStore(string partitionName)   {      // create String array to hold error message      String[] errorMessage = new String[1];      // create partition folder path and check if it exists      String filePath = Path.Combine(cacheRootFilePath,          partitionName);      if (!Directory.Exists(filePath))      {         // this method is called when the Caching App Block first         // accesses the cache, so the code must create the          // directory for the specified partition to allow the          // block to work with it         try         {            Directory.CreateDirectory(filePath);         }         catch (Exception ex)         {            errorMessage[0] = "Cannot create partition folder" +                ex.Message;            return errorMessage;         }      }      ...

Notice how the code creates a one-dimensional String Array to hold any error message at the start of the process, and returns this if an error occurs. The backing store provider uses the array size as an indicator that an error occurred in the web service, and extracts the message it contains.

The next stage is to get a list of all the cached items using the Directory.GetFiles method:

      ...      // create file specification to search for cache files      String searchString = String.Concat("*", dataExtension);      String[] cacheFiles;      try      {         // get a list of cache data files         cacheFiles = Directory.GetFiles(filePath, searchString,            SearchOption.TopDirectoryOnly);      }      catch (Exception ex)      {         errorMessage[0] = "Cannot access cache partition" +             ex.Message;         return errorMessage;      }      ...

Finally, the code iterates through the array of file names extracting the required data from each one and adding that to the String Array that it returns. The information file provides an array containing the key, "last-accessed" date/time, and cache duration that the code can insert directly into the return String Array. The remainder of the method encodes the contents of the data file to Base64, and inserts the result into the array. After processing all the cached items, the code returns the array to the backing store provider:

      ...      if (cacheFiles.Length > 0)      {         try         {            // create String array to contain cached item             // information and data             // The array contains four entries for each cache             // item and repeats in multiples of 4:            // @ Index + 0 = cache key            // @ Index + 1 = last accessed date/time            // @ Index + 2 = duration (seconds)            // @ Index + 3 = Base64 encoded data            String[] cachedItems = new String[               cacheFiles.Length * 4];            Int32 itemIndex = 0;            // iterate through cached files adding data to             // the String array            foreach (String cacheFile in cacheFiles)            {               // create path and file name for information file               String infoName = String.Concat(                  Path.GetFileNameWithoutExtension(cacheFile),                   infoExtension);               String infoPath = Path.Combine(filePath, infoName);               // read from information file               // does not support callbacks or priorities -                // it uses standard values               String[] infoData = File.ReadAllLines(infoPath);               cachedItems[itemIndex] = infoData[0];               cachedItems[itemIndex + 1] = infoData[1];               cachedItems[itemIndex + 2] = infoData[2];               // base64-encode object from .cachefile file               Byte[] itemBytes = File.ReadAllBytes(                  Path.Combine(filePath, cacheFile));               cachedItems[itemIndex + 3] =                   Convert.ToBase64String(itemBytes);               itemIndex += 4;            }            return cachedItems;         }         catch (Exception ex)         {            errorMessage[0] = "Error reading cached items" +                ex.Message;
?
Figure 4. Testing in a Browser: You can view and test most of the Caching Web Service methods in a browser.
return errorMessage; } } else { return null; // no cached items } }

After you finish building the web service, you can test it by opening it in a browser, as shown in Figure 4. You can test all of the methods except for the AddNewItem method, although with no cached items you will generally only get error messages. However, the LoadDataFromStore method will create the specified partition subfolder even though there are no items to return.

Using the Web Service Cache Provider in ASP.NET
The sample code for this article contains a simple ASP.NET application that you can use to test the web service-based caching mechanism. The default configuration specifies two instances of the custom Web Service Caching provider, with different values for the partition name.

Author's Note: Before you use the test application, you must ensure that the URL of the target web service for both backing store instances points to the location of the Caching Web Service (SampleCacheWebService) you want to use. The easiest way to set this up is to install the SampleCacheWebService into Internet Information Services (IIS)?this can be on the same machine or a remote machine. After installing and testing the web service by opening it in a web browser, copy the URL into the test web site configuration. You can edit the configuration file (web.config) using the version of the Configuration Console in your EntLibSrcApp Blocksin folder, or directly in a text editor. Also ensure that the server running the web service has a C:Temp folder available with permission for the ASPNET or NETWORK SERVICE account to read, write, and delete files, and create subfolders. You can edit the path in the web service class file to use a different folder for the cache root if you prefer.

Here's the relevant section of the web.config file for the test web site, showing the values for the two Web Service Cache backing store providers (note that some lines have wrapped in the listing due to the limitations of page width):

                
?
Figure 5. Sample ASP.NET Application: You can use this application to test the custom web service-based caching mechanism.

The page Default.aspx (in the TestWebSite subfolder of the samples) contains two option buttons that let you select which of the two configured instances of the custom Web Service Caching provider you want to use. After the option buttons, you'll see a series of buttons and text boxes where you can enter the name of the cache key and (for a text or numeric type) the value to cache.

You can cache Strings, Doubles, Object Arrays, or a DataSet. Each has different cache durations; the Object Array uses the default duration setting of the selected provider.

When you click one of the buttons to cache a value or an object, the page displays the number of items currently in the selected cache partition, as you can see in Figure 5.

You can also retrieve and display items you previously cached, or remove them from the cache. When you remove an item, the page again displays the number of items currently in the cache. You can also remove all items from the cache. If you try to retrieve or remove an item that does not exist (either because you already removed it or it has expired), the page displays a suitable error message. It also displays details of any other exceptions that may occur?for example, if you specify an incorrect URL for a provider or the provider encounters an error.

Adding Items to the Cache in the Sample Application
The sample page's code-behind is relatively simple. For example, this is the handler for the first button on the page, which caches a String value:

   protected void btn_CacheString_Click(      object sender, EventArgs e)   {      try      {         // create the selected cache manager instance         CacheManager cm = CacheFactory.GetCacheManager(            optCacheManager.SelectedValue);         // cache the item with a short sliding expiry duration         cm.Add(txtStringKey.Text, txtStringValue.Text,             CacheItemPriority.Normal, null, new SlidingTime(            new TimeSpan(0, 0, 20)));         // display number of items in cache and set list items         DisplayResultsAndUpdateButtonLists(cm.Count,             txtStringKey.Text);      }      catch (Exception ex)      {         lblResults.Text = "ERROR: " + ex.Message;      }   }

The code creates an instance of the appropriate CacheManager by using the value selected in the RadioButtonList control at the top of the page. It then calls the Add method, passing the name of the cache key and the value from the text boxes on the page, the cache priority (although this implementation of the provider always uses Normal priority, irrespective of the value you specify here), null for the call-back parameter, and a 20-second sliding time expiry value.

The final step is a call to the DisplayResultsAndUpdateButtonLists routine located at the end of the page code that passes the number of items in the cache (obtained from the CacheManager.Count property) and the name of the key for the item just added. The routine (shown below), uses these values to display the number of items in the cache, update the two lists of cache keys for the Retrieve and Remove buttons, and set the Enabled property of these two buttons to the appropriate value:

   private void DisplayResultsAndUpdateButtonLists(      Int32 cacheCount, String keyText)   {      // display number of items in cache      lblResults.Text = String.Format(         "Cache '{0}' contains {1} item(s).",         optCacheManager.SelectedValue, cacheCount.ToString());      // add the current key to the "Retrieve" list      if (! lstRetrieveItem.Items.Contains(new ListItem(keyText)))      {         lstRetrieveItem.Items.Add(keyText);      }      // set enabled state of "Retrieve" button      btn_Retrieve.Enabled = (lstRetrieveItem.Items.Count != 0);      // add the current key to the "Remove" list      if (!lstRemoveItem.Items.Contains(new ListItem(keyText)))      {         lstRemoveItem.Items.Add(keyText);      }      // set enabled state of "Remove" button      btn_Remove.Enabled = (lstRemoveItem.Items.Count != 0);   }

The button handlers that add Doubles, Object Arrays, and DataSets to the cache are much the same as that for the String value. The handler for a numeric value converts the String from the text box into a Double type using the Double.Parse method, while the code in the handler that caches an Object Array just creates the array before caching it. Notice that it takes advantage of the short form of the Add method, which uses default values for the cache priority and duration:

   ...   // populate an Object Array   Object[] vals = new Object[3];   vals[0] = "Some text";   vals[1] = 42;   vals[2] = DateTime.Now;   // create the selected cache manager instance   CacheManager cm = CacheFactory.GetCacheManager(      optCacheManager.SelectedValue);   // cache the item with the default sliding expiry duration   cm.Add(txtArrayKey.Text, vals);   ...

The code that caches a DataSet creates the DataSet by loading an XML schema and an XML document from the xmldata subfolder of the examples.

Retrieving Items from the Cache
In the sample application, the handler for the button that retrieves items from the cache is somewhat more complex, because it must discover the type of the cached item and convert it for display. The CacheManager's GetData method returns all cached objects as an Object reference type.

After creating the appropriate CacheManager instance, the code first checks whether the item with the key selected in the drop-down list next to the Retrieve button exists in the cache. This check saves a call to the backing store methods if the item is already in the synchronized in-memory cache. When it is, the code calls the GetData method, and then uses a multiple if statement to check the type and display it in the appropriate way.

   protected void btn_Retrieve_Click(object sender, EventArgs e)   {      try      {         // create the selected cache manager instance         CacheManager cm = CacheFactory.GetCacheManager(            optCacheManager.SelectedValue);         // retrieve the item with the specified key         String itemKey = lstRetrieveItem.SelectedValue;         // see if the item is in the cache         if (cm.Contains(itemKey))         {            // check the type and display as appropriate            Object item = cm.GetData(itemKey);            if (item is DataSet)            {               // bind to GridView control               GridView1.DataSource = item as DataSet;               GridView1.DataBind();            }            else if (item is Object[])           {              // iterate array displaying values              StringBuilder builder = new StringBuilder(                "Retrieved item values are: ");                foreach (Object arrayItem in (Object[])item)                {                   builder.Append(arrayItem.ToString());                   builder.Append(", ");                }                lblResults.Text = builder.ToString();           }           else           {              // display the value              lblResults.Text = String.Format(                 "Retrieved item is '{0}'",                 item.ToString());           }        }        else        {           lblResults.Text = String.Format(              "Cache does not contain an item"              + " with key '{0}'.", itemKey);        }     }     catch (Exception ex)     {        lblResults.Text = "ERROR: " + ex.Message;     }   }

Figure 6 shows the result of retrieving an Object Array from the cache.

?
Figure 6. Retrieving a Cached Object Array: The line at the bottom shows the result of retrieving a cached Object Array.

Removing Items from the Cache in the Sample Application
The sample application allows you to remove individual items from the cache by specifying a cache key, or flush the cache to remove all items (see the last two buttons in Figure 6). As you will have guessed, the code in the handlers for these buttons just has to call the appropriate CacheManager method.

To remove a single item, the code creates an instance of the specified CacheManager, calls the Contains method to see if the item is in the cache, and?if it is?calls the Remove method with the cache key selected in the list next to the Remove button:

   ...   // create the selected cache manager instance   CacheManager cm = CacheFactory.GetCacheManager(      optCacheManager.SelectedValue);   // remove the item with the specified key   String itemKey = lstRemoveItem.SelectedValue;   if (cm.Contains(itemKey))   {      cm.Remove(itemKey);   }   else   {      lblResults.Text = String.Format(         "Cache does not contain an item "         + "with key '{0}'.", itemKey);   }   // display number of items in cache   lblResults.Text = String.Format(      "Cache '{0}' contains {1} item(s).",      optCacheManager.SelectedValue, cm.Count.ToString());   ...

Clicking the "Remove all items" button creates an instance of the specified CacheManager and calls the Flush method:

   ...   // create the selected cache manager instance   CacheManager cm = CacheFactory.GetCacheManager(      optCacheManager.SelectedValue);   // clear the cache   cm.Flush();   // display number of items in cache   lblResults.Text = String.Format(      "Cache '{0}' contains {1} item(s).",      optCacheManager.SelectedValue, cm.Count.ToString());   // disable "Retrieve" and "Remove" buttons   btn_Retrieve.Enabled = false;   btn_Remove.Enabled = false;   ...

Notice that none of the routines in the test web site require you to specify the cache partition name. The partition name, like the URL of the target Caching Web Service, is part of the backing store provider configuration. Therefore, by specifying the backing store provider you want to use in the GetCacheManager method, the system automatically reads from and writes to the correct cache partition.

You can add multiple Cache Managers and Cache Backing Store providers to an application configuration, including providers of different types, to meet almost any combination of caching requirements. If you need to support multiple caching web services, and multiple cache partitions, just add a separate Web Service Caching Provider for each combination.

Possible enhancements
While there are some limitations in the current implementation, you can easily add features you require to the base model described here.

You could extend the sample Web Service Cache Provider and make it even more useful by adding support for:

  • Credentials that the proxy will use to access non-anonymous web services.
  • An encryption provider to encrypt the data passing over the wire?although you can use SSL to communicate with the web service without adding this feature. You could implement encryption using the same techniques as in the Isolated Storage provider that is part of the Caching Application Block.
  • Additional cache priorities.

You can use the caching mechanism described in this article to extend the capabilities of the Enterprise Library Caching Application Block to support almost any scenario for caching data remotely. While this project began as a "blue sky" concept implemented purely to see if it was possible, it does have practical applications and provides an interesting (and possibly useful) mechanism that you may want to take advantage of in your applications.

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