Using this question as a base, and using some of the advice in the answers, I wanted to build out something that would be generic, thread-safe, and easy to use for at least one current and several future projects.
The idea is to be able to call one function, passing a key and passing another function to generate the data, if needed. It returns true/false to indicate success and saves the data to an OUT parameter. There's a default cache time but also overload methods that allow the developer to pass a new absolute duration (in minutes).
The class and functions work, I can step through the code and see that it hits the passed function the first time, then skips it in subsequent requests.
The external static call to IsCachingEnabled is basically just looking for an AppSetting in the web.config.
Some functions are marked internal but could be set to public - they're there partly as an offshoot of the original sample and partly because I may enable them in the future. For my current project, I'm only interested in using the TryGetAndSet method from outside.
Here's the code. I'm interested to hear whether anything here just doesn't make sense, isn't thread-safe, or is just plain bad practice.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Caching;
using System.Collections.Concurrent;
namespace Ektron.Com
{
/// <summary>
/// Uses System.Runtime.Caching to provide a thread-safe caching class.
/// Recommended use is to employ the TryGetAndSet method as a wrapper to call
/// your data-building function.
/// </summary>
public static class CacheManager
{
/// <summary>
/// The cache store. A dictionary that stores different memory caches by the type being cached.
/// </summary>
private static ConcurrentDictionary<Type, ObjectCache> cacheStore;
/// <summary>
/// The default minutes (15)
/// </summary>
private const int DefaultMinutes = 15;
#region constructors
/// <summary>
/// Initializes the <see cref="CacheManager"/> class.
/// </summary>
static CacheManager()
{
cacheStore = new ConcurrentDictionary<Type, ObjectCache>();
}
#endregion
#region Setters
/// <summary>
/// Sets the specified cache using the default absolute timeout of 15 minutes.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cacheKey">The cache key.</param>
/// <param name="cacheItem">The data to be cached.</param>
static internal void Set<T>(string cacheKey, T cacheItem)
{
Set<T>(cacheKey, cacheItem, DefaultMinutes);
}
/// <summary>
/// Sets the specified cache using the absolute timeout specified in minutes.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cacheKey">The cache key.</param>
/// <param name="cacheItem">The data to be cached.</param>
/// <param name="minutes">The absolute expiration (in minutes).</param>
static internal void Set<T>(string cacheKey, T cacheItem, int minutes)
{
if (Ektron.Com.Helpers.Constants.IsCachingEnabled)
{
Type t = typeof(T);
if (!cacheStore.ContainsKey(t))
{
RegisterCache(t);
}
var cache = cacheStore[t];
cache.Set(cacheKey, cacheItem, GetCacheItemPolicy(minutes));
}
}
/// <summary>
/// Sets the specified cache using the passed function to generate the data.
/// Uses default absolute timeout of 15 minutes.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cacheKey">The cache key.</param>
/// <param name="getData">The function to generate the data to be cached.</param>
static internal void Set<T>(string cacheKey, Func<T> getData)
{
Set<T>(cacheKey, getData, DefaultMinutes);
}
/// <summary>
/// Sets the specified cache using the passed function to generate the data.
/// Uses the specified absolute timeout (in minutes).
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cacheKey">The cache key.</param>
/// <param name="getData">The function to generate the data to be cached.</param>
/// <param name="minutes">The absolute expiration (in minutes).</param>
static internal void Set<T>(string cacheKey, Func<T> getData, int minutes)
{
if (Ektron.Com.Helpers.Constants.IsCachingEnabled)
{
Type t = typeof(T);
if (!cacheStore.ContainsKey(t))
{
RegisterCache(t);
}
var cache = cacheStore[t];
T data = getData();
cache.Set(cacheKey, data, GetCacheItemPolicy(minutes));
}
}
#endregion
#region Getters
/// <summary>
/// Tries to retrieve data from cache first. If the data is not found in cache, the passed function
/// will be used to generate and store the data in cache. Data is returned via the returnData parameter.
/// Function returns true if successful.
/// Uses the default absolute timeout of 15 minutes.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cacheKey">The cache key.</param>
/// <param name="getData">The function to generate the data to be cached.</param>
/// <param name="returnData">The return data.</param>
/// <returns>True if successful. False if data is null.</returns>
public static bool TryGetAndSet<T>(string cacheKey, Func<T> getData, out T returnData)
{
if (!Ektron.Com.Helpers.Constants.IsCachingEnabled)
{
Remove<T>(cacheKey);
}
Type t = typeof(T);
bool retrievedFromCache = TryGet<T>(cacheKey, out returnData);
if (retrievedFromCache)
{
return true;
}
else
{
returnData = getData();
Set<T>(cacheKey, returnData);
return returnData != null;
}
}
/// <summary>
/// Tries to retrieve data from cache first. If the data is not found in cache, the passed function
/// will be used to generate and store the data in cache. Data is returned via the returnData parameter.
/// Function returns true if successful.
/// Uses the specified absolute timeout (in minutes).
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cacheKey">The cache key.</param>
/// <param name="getData">The function to generate the data to be cached.</param>
/// <param name="minutes">The absolute expiration (in minutes).</param>
/// <param name="returnData">The return data.</param>
/// <returns>True if successful. False if data is null.</returns>
public static bool TryGetAndSet<T>(string cacheKey, Func<T> getData, int minutes, out T returnData)
{
Type t = typeof(T);
bool retrievedFromCache = TryGet<T>(cacheKey, out returnData);
if (retrievedFromCache && Ektron.Com.Helpers.Constants.IsCachingEnabled)
{
return true;
}
else
{
returnData = getData();
Set<T>(cacheKey, returnData, minutes);
return returnData != null;
}
}
/// <summary>
/// Attempts to retrieve data from cache.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cacheKey">The cache key.</param>
/// <param name="returnItem">The data from cache.</param>
/// <returns>True if successful. False if data is null or not found.</returns>
static internal bool TryGet<T>(string cacheKey, out T returnItem)
{
Type t = typeof(T);
if (cacheStore.ContainsKey(t))
{
var cache = cacheStore[t];
object tmp = cache[cacheKey];
if (tmp != null)
{
returnItem = (T)tmp;
return true;
}
}
returnItem = default(T);
return false;
}
#endregion
/// <summary>
/// Removes the specified item from cache.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cacheKey">The cache key.</param>
static internal void Remove<T>(string cacheKey)
{
Type t = typeof(T);
if (cacheStore.ContainsKey(t))
{
var cache = cacheStore[t];
cache.Remove(cacheKey);
}
}
/// <summary>
/// Registers the cache in the dictionary.
/// </summary>
/// <param name="t">The type used as the key for the MemoryCache that stores this type of data.</param>
private static void RegisterCache(Type t)
{
ObjectCache newCache = new MemoryCache(t.ToString());
cacheStore.AddOrUpdate(t, newCache, UpdateItem);
}
/// <summary>
/// Updates the item. Required for use of the ConcurrentDictionary type to make this thread-safe.
/// </summary>
/// <param name="t">The Type used as the key for the MemoryCache that stores this type of data.</param>
/// <param name="cache">The cache to be updated.</param>
/// <returns></returns>
private static ObjectCache UpdateItem(Type t, ObjectCache cache)
{
var newCache = new MemoryCache(cache.Name);
foreach (var cachedItem in cache)
{
newCache.Add(cachedItem.Key, cachedItem.Value, GetCacheItemPolicy(DefaultMinutes));
}
return newCache;
}
/// <summary>
/// Gets the cache item policy.
/// </summary>
/// <param name="minutes">The absolute expiration, in minutes.</param>
/// <returns>A standard CacheItemPolicy, varying only in expiration duration, for all items stored in MemoryCache.</returns>
private static CacheItemPolicy GetCacheItemPolicy(int minutes = 15)
{
var policy = new CacheItemPolicy()
{
Priority = CacheItemPriority.Default,
AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(minutes)
};
return policy;
}
}
}
if
statements. It's easier to return from a quickif
than to wrap the contents of entire methods in them. Makes code more readable, and less layers of "nestiness"... \$\endgroup\$