At some point in an application’s development, usually, fairly early on, you realize the application is slow. After some research, the culprit is, unnecessarily retrieving the same data, and a light goes off, and you think: “I need some caching.”
Caching is an invaluable pattern for eliminating redundant calls to a database or a third party API. Microsoft provides IMemoryCache
for time-based caching, however sometimes time-based caching isn’t what you need. In this article, we look at Request Scoped caching and how it can benefit us.
What is Request caching? Request caching is a mechanism to cache data for the life of a web request. In dot-net, we’ve had this ability in some capacity with the HttpContext.Items collection, however, HttpContext
is not known for its injectability.
Request Scoped caching has a few benefits: First, it eliminates the concern of stale data. In most scenarios, a request executes in less than a second and which typically isn’t long enough for data to become stale. And secondly, expiration isn’t a concern because the data dies when the request ends.
Out of the box, Asp.Net Core doesn’t have injectable caching. As mentioned earlier, HttpContext.Items
is an option, but it’s not an elegant solution.
Luckily for us, ASP.Net Core gives us the tools to create an injectable Request Caching implementation by using the built-in dependency injection (DI) framework.
The built-in DI framework has three lifetimes for dependencies: Singleton
, Scoped
, and Transient
. Singleton
is for the life of the application, Scoped is for the life of the request and Transient is a new instance with each request.
I’ve created an interface modeled after the IMemoryCache
interface to keep things consistent.
Interface
public interface IRequestCache
{
/// <summary>
/// Add the value into request cache. If the key already exists, the value is overwritten.
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <typeparam name="TValue"></typeparam>
void Add<TValue>(string key, TValue value);
/// <summary>
/// Remove the key from the request cache
/// </summary>
/// <param name="key"></param>
void Remove(string key);
/// <summary>
/// Retrieve the value by key, if the key is not in the cache then the add func is called
/// adding the value to cache and returning the added value.
/// </summary>
/// <param name="key"></param>
/// <param name="add"></param>
/// <typeparam name="TValue"></typeparam>
/// <returns></returns>
TValue RetrieveOrAdd<TValue>(string key, Func<TValue> add);
/// <summary>
/// Retrieves the value by key. When the key does not exist the default value for the type is returned.
/// </summary>
/// <param name="key"></param>
/// <typeparam name="TValue"></typeparam>
/// <returns></returns>
TValue Retrieve<TValue>(string key);
}
Implementation
public class RequestCache : IRequestCache
{
IDictionary<string, object> _cache = new Dictionary<string, object>();
/// <summary>
/// Add the value into request cache. If the key already exists, the value is overwritten.
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <typeparam name="TValue"></typeparam>
public void Add<TValue>(string key, TValue value)
{
_cache[key] = value;
}
/// <summary>
/// Remove the key from the request cache
/// </summary>
/// <param name="key"></param>
public void Remove(string key)
{
if (_cache.ContainsKey(key))
{
_cache.Remove(key);
}
}
/// <summary>
/// Retrieve the value by key, if the key is not in the cache then the add func is called
/// adding the value to cache and returning the added value.
/// </summary>
/// <param name="key"></param>
/// <param name="add"></param>
/// <typeparam name="TValue"></typeparam>
/// <returns></returns>
public TValue RetrieveOrAdd<TValue>(string key, Func<TValue> add)
{
if (_cache.ContainsKey(key))
{
return (TValue)_cache[key];
}
var value = add();
_cache[key] = value;
return value;
}
/// <summary>
/// Retrieves the value by key. When the key does not exist the default value for the type is returned.
/// </summary>
/// <param name="key"></param>
/// <typeparam name="TValue"></typeparam>
/// <returns></returns>
public TValue Retrieve<TValue>(string key)
{
if (_cache.ContainsKey(key))
{
return (TValue)_cache[key];
}
return default(TValue);
}
}
Using ASP.Net Core’s DI framework we’ll wire it up as Scoped
.
services.AddScoped<IRequestCache, RequestCache>();
Usage
public class UserService
{
private readonly IRequestCache _cache;
private readonly IUserRepository _userRepository;
public UserService(IRequestCache cache, IUserRepository userRepository)
{
_cache = cache;
_userRepository = userRepository;
}
public User RetrieveUserById(int userId)
{
var buildCacheKey = UserService.BuildCacheKey(userId);
return _cache.RetrieveOrAdd(BuildCacheKey, () => { return _userRepository.RetrieveUserBy(userId); });
}
public void Delete(int userId)
{
var buildCacheKey = UserService.BuildCacheKey(userId);
_userRepository.Delete(userId);
_cache.Remove(BuildCacheKey(userId));
}
private static string BuildCacheKey(int userId)
{
return $"user_{userId}";
}
}
That’s it! Request Caching is now injectable in any place you need it.
Visit the Git Repository and feel free to take the code for a spin.