Browse DevX
Sign up for e-mail newsletters from DevX


Caching Data with a Web Service in Enterprise Library : Page 6

If you've ever wondered whether you might be able to use a web service to cache data—or whether it would be fast enough to be useful—wonder no more.




Building the Right Environment to Support AI, Machine Learning and Deep Learning

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.

Thanks for your registration, follow us on our social networks to keep up-to-date