Exercise 3: Creating a Reusable and Extensible Caching Layer
In the previous exercise, you explored the fundamental aspects of using the Windows Azure Caching by directly updating the methods in the data access class to cache data retrieved from the repository. While this approach can yield significant benefits, it requires you to change each one of your data access methods to enable caching. An alternative approach that does not require changes to your existing data access classes would be advantageous.
In this exercise, you will explore building a caching layer on top of your existing data access classes that will allow you to plug in different caching providers, or even remove them altogether, through simple configuration changes.
To build this layer, you will implement an abstract caching class named CachedDataSource that will provide support for storing and removing data in the cache. You will then derive from this class to create a caching equivalent for any data source in your application. The only requirement is that your data source implements a contract to define its data access operations. The caching class encapsulates a caching provider, which you need to provide in its constructor, and provides methods to retrieve and remove data from the cache.
The data retrieval method in the caching class receives a cache key that uniquely identifies a cached item, a delegate that retrieves data from the data source, and a cache expiration policy that determines when to purge the item from the cache. This method implements the classic caching pattern where it first attempts to retrieve an item from the cache and, if it does not find a copy, uses the supplied delegate to retrieve the data from the source and then stores it in the cache.
The implementation of the CachedDataSource class is completely reusable, allowing you to use any caching provider that fits your requirements. To specify a caching provider, you supply an ObjectCache instance to its constructor. The ObjectCache class, part of the System.Runtime.Caching namespace, was introduced in the .NET Framework 4 to make caching available for all applications. This abstract class represents an object cache and provides base methods and properties for accessing an underlying cache provider. The .NET Framework already offers a concrete implementation of this class that provides an in-memory cache, the MemoryCache.
To use a given cache service with the CachedDataSource derived class, you need to supply an ObjectCache implementation specific to the caching provider. A good approach is to create a data source factory that allows you to choose a suitable caching implementation based on your needs. Replacing the caching provider is then simply a matter of changing a setting in the configuration file.
Currently, the Windows Azure Caching does not supply its own ObjectCache implementation. Nevertheless, you can create one that provides a wrapper around its services. You will find an example of such an implementation, the AzureCacheProvider, in the BuildingAppsWithCacheService\Source\Assets folder. This class derives from ObjectCache to expose the services in the Windows Azure Caching.
To take advantage of this caching implementation in the Azure Store application, you will create a caching counterpart of the ProductsRepository class. The application uses this class, which implements an IProductsRepository contract with a single GetProducts operation, to retrieve catalog information from SQL Azure. To create a caching products catalog source, you need to perform the following steps:
- Create a new CachingProductsReposity class that inherits from CachedDataSource.
- Add a constructor to the new class that receives an IProductRepository parameter with an instance of the non-caching data source class as well as an ObjectCache parameter with an instance of the caching provider to use.
- Implement each method in the IProductRepository interface by calling the RetrievedCachedData method in the base class and supplying a delegate that calls the original data source class.
Task 1 – Implementing a Caching Data Source Base Class
In this task, you will create the abstract class that you will use as the base class for your caching data source classes. You can take advantage of this general-purpose class in any project that requires a caching layer.
- Start Microsoft Visual Studio 2010 as administrator.
- Open the Begin solution located at Source\Ex3-ReusableCachingImplementation.
Important: Before you execute the solution, make sure that the start-up project is set. For MVC projects, the start page must be left blank.
To set the start up project, in
Solution Explorer, right-click the
AzureStoreService project and then select
Set as StartUp Project.
To set the start page, in
Solution Explorer, right-click the
MVCAzureStore project and select
Properties. In the
Properties window, select the
Web tab and in the
Start Action, select
Specific Page. Leave the value of this field blank.
- In the Web.config file, update the NorthwindEntities connection string to point to your database. Replace [YOUR-SQL-AZURE-SERVER-ADDRESS], [SQL-AZURE-USERNAME], and [SQL-AZURE-PASSWORD] with the SQL Azure database server name, Administrator Username and Administrator password that you registered at the portal and used for creating the database during setup.
Make sure that you follow the instructions of the setup section to create a copy of the Northwind2 database in your own SQL Azure account and configure your SQL Azure firewall settings.
- Add a reference to the System.Runtime.Caching assembly in the MVCAzureStore project.
- In the Services folder of the MVCAzureStore project, add a new folder named Caching.
- Inside the Caching folder created in the previous step, add a new class file named CachedDataSource.cs.
- In the new class file, add a namespace directive for System.Runtime.Caching.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Caching;
...
- Specify an abstract modifier for the CachedDataSource class.
public abstract class CachedDataSource
{}
- Add the following (highlighted) member fields to the class.
(Code Snippet – BuildingAppsWithCachingService-Ex3-CachedDataSource member fields-CS)
public abstract class CachedDataSource
{ private readonly ObjectCache cacheProvider;
private readonly string regionName;
}
- Now, define a constructor that receives an object cache and a region name as parameters, as shown (highlighted) below.
(Code Snippet – BuildingAppsWithCachingService-Ex3-CachedDataSource constructor-CS)
public abstract class CachedDataSource
{ ...
public CachedDataSource(ObjectCache cacheProvider, string regionName)
{
if (cacheProvider == null)
{
throw new ArgumentNullException("cacheProvider");
}
if (cacheProvider is MemoryCache)
{
regionName = null;
}
this.cacheProvider = cacheProvider;
this.regionName = regionName;
}
}
The
CachedDataSource constructor receives an
ObjectCache instance as a parameter, which provides methods and properties for accessing an object cache, as well as a region name. A cache region is a partition in the cache used to organize cache objects.
- Next, add the following (highlighted) method to retrieve data from the cache.
(Code Snippet – BuildingAppsWithCachingService-Ex3-RetrieveCachedData method-CS)
public abstract class CachedDataSource
{ ...
protected T RetrieveCachedData<T>(string cacheKey, Func<T> fallbackFunction, CacheItemPolicy cachePolicy) where T : class
{
var data = this.cacheProvider.Get(cacheKey, this.regionName) as T;
if (data != null)
{
return data;
}
data = fallbackFunction();
if (data != null)
{
this.cacheProvider.Add(new CacheItem(cacheKey, data, this.regionName), cachePolicy);
}
return data;
}
}
The RetrieveCachedData method uses the provided key to retrieve a copy of the requested item from the cache. If the data is available, it returns it; otherwise, it uses the provided fallback delegate to obtain the information from the data source and then caches the result using the supplied cache expiration policy.
- Finally, add a method to delete items from the cache.
(Code Snippet – BuildingAppsWithCachingService-Ex3-RemoveCachedData method-CS)
public abstract class CachedDataSource
{ ...
protected void RemoveCachedData(string cacheKey)
{
this.cacheProvider.Remove(cacheKey, this.regionName);
}
}
- Save the CachedDataSource.cs file.
Task 2 – Building a Caching Product Catalog Repository
Once you have created an abstract base class for caching data sources, you will now create a concrete implementation that will provide a caching alternative for the ProductsRepository class. This task represents the steps you would typically follow when creating a caching layer for your data access code using the CachedDataSource class.
- Inside the Services\Caching folder of the MVCAzureStore project, add a new class file named CachedProductsRepository.cs.
- In the new class file, append a namespace directive for System.Runtime.Caching.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Caching;
...
- Change the declaration for the CachedProductsRepository class to derive from both CachedDataSource and IProductRepository, as shown (highlighted) below.
public class CachedProductsRepository
: CachedDataSource, IProductRepository
{}
The caching data source class derives from CachedDataSource to provide the necessary caching behavior, as well as implementing the same contract used by the original data source class.
- Add the following code to define a constructor and declare a member field that holds a reference to the underlying data source, as shown (highlighted) below.
(Code Snippet – BuildingAppsWithCachingService-Ex3-CachedProductsRepository constructor-CS)
public class CachedProductsRepository : CachedDataSource, IProductRepository
{ private readonly IProductRepository repository;
public CachedProductsRepository(IProductRepository repository, ObjectCache cacheProvider) :
base(cacheProvider, "Products")
{
this.repository = repository;
}
}
The CachedProductsRepository constructor initializes its base class using the supplied cache provider and saves a reference to the underlying data source in a member field. The class defines a “Products” cache region.
- Finally, fulfill the IProductRepository contract by implementing the GetProducts method, as shown (highlighted) below.
(Code Snippet – BuildingAppsWithCachingService-Ex3-GetProducts method -CS)
public class CachedProductsRepository : CachedDataSource, IProductRepository
{ ...
public List<string> GetProducts()
{
return RetrieveCachedData(
"allproducts",
() => this.repository.GetProducts(),
new CacheItemPolicy { AbsoluteExpiration = DateTime.UtcNow.AddMinutes(1) });
}
}
The
GetProducts method calls
RetrieveCachedData in the base class, passing in a key that identifies the cached item, in this case “
allproducts”, a fallback delegate in the form of a lambda expression that simply calls the
GetProducts method in the original data source, and a
CacheItemPolicy to set the expiration of the item to 1 minute.
Because the
IProductRepository contract is so simple, this is all that is required to provide a caching implementation. Typically, your data sources will have more than one method, but the basic approach should not change, allowing you to implement every method by copying this same pattern.
Task 3 – Creating a Data Source Factory Class
In this task, you will create a factory class that can return data source instances. The factory determines the cache provider to use from the application configuration settings and returns a data source suitably configured to use the chosen cache provider.
- Add a copy of the AzureCacheProvider.cs file located in the \Source\Assets folder to the MVCAzureStore project and place it in its Services\Caching folder.
The AzureCacheProvider class implements an ObjectCache that wraps the services provided by the Windows Azure Cache Service.
- Inside the Services folder of the MVCAzureStore project, add a new class file named DataSourceFactory.cs.
- In the new class file, insert namespace directives for System.Configuration, System.Runtime.Caching, and MVCAzureStore.Services.Caching.
(Code Snippet – BuildingAppsWithCachingService-Ex3-DataSourceFactory namespaces-CS)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Configuration;
using System.Runtime.Caching;
using MVCAzureStore.Services.Caching;
- Now, add the following code to define a type constructor for the DataSourceFactory class and declare a static field that holds a reference to the configured cache service provider, as shown (highlighted) below.
(Code Snippet – BuildingAppsWithCachingService-Ex3-DataSourceFactory class constructor-CS)
public class DataSourceFactory
{ private static readonly ObjectCache cacheProvider;
static DataSourceFactory()
{
string provider = ConfigurationManager.AppSettings["CacheService.Provider"];
if (provider != null)
{
switch (ConfigurationManager.AppSettings["CacheService.Provider"].ToUpperInvariant())
{
case "AZURE":
cacheProvider = new AzureCacheProvider();
break;
case "INMEMORY":
cacheProvider = MemoryCache.Default;
break;
}
}
}
}
The class constructor reads the CacheService.Provider setting from the configuration and initializes the cache provider for the application based on its value. In this example, two different values for the setting are recognized, one for the Windows Azure Caching and another one for the default in-memory cache provider offered by the .NET Framework 4.
- Next, add the following property to return the configured cache service provider.
(Code Snippet – BuildingAppsWithCachingService-Ex3- CacheProvider property-CS)
public class DataSourceFactory
{ ...
public static ObjectCache CacheProvider
{
get { return cacheProvider; }
}
}
- Finally, add a method to return an instance of the IProductRepository data source initialized with the configured cache service provider.
(Code Snippet – BuildingAppsWithCachingService-Ex3-GetProductsRepository method-CS)
public class DataSourceFactory
{ ...
public static IProductRepository GetProductsRepository(bool enableCache)
{
var dataSource = new ProductsRepository();
if (enableCache && CacheProvider != null)
{
return new CachedProductsRepository(dataSource, cacheProvider);
}
return dataSource;
}
}
Task 4 – Configuring the Application for Caching
In this task, you will update the application to take advantage of the data source factory to instantiate the product catalog data source. To complete the setup of the caching layer, you will define the necessary configuration settings to select a caching provider.
- Open the HomeController.cs file in the Controllers folder and find the Index method. Inside this method, replace the line that initializes the productRepository local variable with the code shown (highlighted) below that uses the DataSourceFactory to retrieve an IProductRepository instance.
public class HomeController : Controller
{ ...
public ActionResult Index()
{ bool enableCache = (bool)this.Session["EnableCache"];
// retrieve product catalog from repository and measure the elapsed time
Services.IProductRepository productRepository =
MVCAzureStore.Services.DataSourceFactory.GetProductsRepository(enableCache);
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
...
}
...
}
- To configure the DataSourceFactory, open the Web.config file and insert the following (highlighted) appSettings section, ensuring that you add the content after the configSections element and as a direct child of the configuration element.
(Code Snippet – BuildingAppsWithCachingService-Ex3-Web.config appSettings section-CS)
<configuration>
<configSections>
...
</configSections>
...
<appSettings>
<add key="CacheService.Provider" value="InMemory" />
</appSettings>
...
</configuration>
If you host the application in a single node, the in-memory cache provider would be a good choice.
- Locate the dataCacheClient section with name “default” and replace the [SERVICE-HOST-NAME] placeholder with the name of the host for the Windows Azure Caching service endpoint that you provisioned earlier. For example, your-namespace.cache.windows.net. Replace the [AUTHORIZATION_INFO] placeholder with the authorization token that you copied from the Cache Settings page.
- Press F5 to build and test the enhanced caching implementation in the compute emulator.
- When you start the application, the cache is initially disabled. Click Yes in Use cache for product data and wait for the page to refresh. Remember that the initial request after you enable the cache includes the overhead required to retrieve the data and insert it into the cache.
- Click Products, or refresh the page in the browser once again. This time, the application retrieves the product data from the cache and the elapsed time should be lower, most likely under a millisecond given that you have currently configured it to use the in-memory cache provided by the .NET Framework.
- Now, in the Web.config file, locate the appSettings section and set the value of the CacheService.Provider setting to Azure.
<configuration>
...
<appSettings>
<add key="CacheService.Provider" value="Azure" />
</appSettings>
...
</configuration>
If you host the application in multiple nodes, the in-memory cache provider is no longer a good choice. Instead, you can take advantage of the distributed cache offered by the Windows Azure Caching.
- Save the Web.config file.
- Suspend and Run the compute emulator to restart and reload the configuration.
- Make sure that the cache is still enabled and then refresh the page in the browser twice to prime the cache with data. Notice that the elapsed times for the cached scenario have increased indicating that the application is now using the Windows Azure Caching provider instead of the in-memory provider.