There are a lot of applications that are built these days with data that doesn't change all that much. A product catalog is usually a good example of that. There may be updates here or there, but not every five minutes. With fairly static data like this, why do we feel the need to make a request from the controller, through a service class of some type, to a repository, which then reaches out to some form of infrastructure such as a database or the filesystem? In cases like these, we can save ourselves a lot of pain by doing that big reach once and then stuffing the results into the cache for a while. With the next request, we can then just fetch the data directly from the cache.
In this recipe, we will see how to create a simple wrapper for the standard .NET cache. We will create the ability to stash and get a single item from the cache. We will also create some methods that allow us to fetch a group of items, as well as store a group of items. Similar to other wrapper classes, the one key thing that we have created for these wrappers to do is expose strongly typed results rather than cache-oriented items. This will mean that our application won't know anything about the idea of cache. Ultimately, this gives us the ability to pick and choose how we want to cache our items down the road.
Product
class to the Models
directory.Models/Product.cs:
public class Product { public string Name { get; set; } public decimal Price { get; set; } }
ProductService
class to help us generate some products to work with. This class will use NBuilder to generate 100 fictitious products to work with.Models/ProductService.cs:
public class ProductService { public List<Product> GetProducts() { List<Product> result = Builder<Product> .CreateListOfSize(100) .Build() .ToList(); return result; } }
public class CacheWrapper public class CacheWrapper { private List<object> GetCacheItems(string[] keys) { IDictionaryEnumerator theCache = HttpContext.Current.Cache.GetEnumerator(); List<object> results = new List<object>(); while (theCache.MoveNext()) { if (keys.Contains(theCache.Key)) results.Add(theCache.Value); } return results; } private object GetCachItem(string key) { if (HttpContext.Current.Cache[key] != null) return HttpContext.Current.Cache[key]; return null; } private void AddCacheItems(Dictionary<string, object> items) { foreach (KeyValuePair<string, object> item in items) { AddCacheItem(item.Key, item.Value); } } private void AddCacheItem(string key, object item) { HttpContext.Current.Cache.Add(key, item, null, //dependencies DateTime.MaxValue, //absolute expiration new TimeSpan(0, 1, 0, 0), //sliding expiration CacheItemPriority.Default, //priority null); //callback } }
Models/CacheWrapper.cs:
public class CacheWrapper { public void PutProducts(List<Product> products) { Dictionary<string, object> itemsToCache = new Dictionary<string, object>(); foreach (Product product in products) { itemsToCache.Add(product.Name, product); } AddCacheItems(itemsToCache); } public List<Product> GetProducts(string[] productNames) { List<Product> results = new List<Product>(); List<object> cacheItems = GetCacheItems(productNames); foreach (object cacheItem in cacheItems) { results.Add(cacheItem as Product); } return results.OrderBy(p => p.Name).ToList(); } ... }
Global.asax:
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
PreLoadProductCatalog();
}
private void PreLoadProductCatalog() {
List<Product> products = new ProductService().GetProducts();
new CacheWrapper().PutProducts(products);
}
Product
catalog is placed firmly into the cache, we are ready to create a new action and view to display our catalog. We will start by first adding a new action called CachedProducts
to the home controller. This action will put in a query to our CacheWrapper
for a select set of products based on their names, and then pass that result down to the view.Controllers/HomeController.cs:
public ActionResult CachedProducts() { List<Product> products = new CacheWrapper().GetProducts(new[] { "Name1", "Name5", "Name98", "Name39", "Name88", "Name34" }); return View(products); }
Product
is in the list of strongly typed items. Set this view to be a list of products.Be aware that, in this recipe, the cached items are set to expire after an hour if they are not used. You would want to expose this property to the methods in your cache wrapper, so that it could be set depending on the required usage. Also, for items that are loaded when the app starts, you would want to make sure that, if the item is not in the cache, you can get them from some other source.
In this recipe, we created a cache wrapper, which we then used to preload our product catalog when the application first started. Then down the road when we accessed our CacheWrapper
to get a list of products, we were working directly with products loaded in memory. This allows you to bypass calls to a database server where you then run possibly complex queries to get a set of products.
Keep in mind that when working with a cached set of data, the data could disappear out from under you. This means that you usually need your full cache implementation (not the cache wrapper per se) to first check the cache for items and then take action upon those results. If cached items are found, they are returned. If they are not found, then you need to get the items in another manner and then cache those results, so that they are ready for the next request.