MemoryCache.cs
public class MemoryCache<TKey, TValue>
{
private static readonly object Lock = new object();
private static readonly IDictionary<TKey, TValue> Cache = new ConcurrentDictionary<TKey, TValue>();
/// <summary>
/// Function for setting the cache's value when the key isn't found.
/// </summary>
public Func<TKey, TValue> Set { get; set; }
/// <summary>
/// Retrieves the value marked by the indicated key in a thread-safe way.
/// If the item is not found, uses Set to retrieve it and store it in the cache before returning the value.
/// </summary>
public TValue Get(TKey key)
{
lock (Lock)
{
if (Cache.TryGetValue(key, out var value) == false)
{
value = Set(key);
Cache.Add(key, value);
}
return value;
}
}
/// <summary>
/// Clears all values in the cache.
/// </summary>
public void Clear()
{
lock (Lock)
{
Cache.Clear();
}
}
}
Usage
Set
is being used to retrieve the customer information from the database via stored procedure. GetBasicCustomer
is a service/repository function for retrieving a customer by customerId
or sapCustomerId
.
public class CustomerService : ICustomerService
{
private static readonly MemoryCache<Tuple<int?, string>, Customer> CustomerCache = new MemoryCache<Tuple<int?, string>, Customer>()
{
Set = (Tuple<int?, string> key) =>
{
var ds = Database.ExecuteStoredProcedure("EXEC usp_Experiment_BasicCustomerInformation @CustomerID, @SAPCustomerID", key.Item1, key.Item2);
if (ds == null || ds.Tables.Count == 0 || ds.Tables[0].Rows.Count == 0)
{
return null;
}
var dr = ds.Tables[0].Rows[0];
return new Customer(
id: dr.Field<int>("CustomerID"),
sapCustomerId: dr.Field<string>("SAPCustomerID"),
companyName: dr.Field<string>("CompanyName"),
isSbtCustomer: dr.Field<bool>("IsSBTCustomer")
);
}
};
/// <summary>
/// Retrieves the customer's basic information given CustomerID or SAPCustomerID (only one is required, make the other null).
/// </summary>
public Customer GetBasicCustomer(int? customerId, string sapCustomerId)
{
var key = new Tuple<int?, string>(customerId, sapCustomerId);
return CustomerCache.Get(key);
}
}
public CustomerController()
{
public ActionResult Index(int? customerId, string sapCustomerId)
{
var customerInfo = CustomerService.GetBasicCustomer(customerId, sapCustomerId);
return View(customerInfo);
}
}
Concerns
- I was thinking
Set
should maybe be namedFindAndSet
or something like that, but not really sure. I like the aesthetic ofGet
/Set
. - Is
ConcurrentDictionary
necessary to be thread-safe since I'm using alock
? Is it doing anything for me?
MemoryCache
class reviewed, I added usage for context, the thing you voted I lacked. So if you could comment and let me know what I'm lacking, that'd be appreciated. \$\endgroup\$ConcurrentDictionart'2.GetOrAdd
has different semantics to theGet
here: the OP will callSet
exactly once, so though it may simplify the implementation,GetOrAdd
will still need wrapping if this behaviour is desirable. \$\endgroup\$