How can I cache objects in ASP.NET MVC?
A couple of the other answers here don't deal with the following:
- cache stampede
- double check lock
This could lead to the generator (which could take a long time) running more than once in different threads.
Here's my version that shouldn't suffer from this problem:
// using System;
// using System.Web.Caching;
// https://stackoverflow.com/a/42443437
// Usage: HttpRuntime.Cache.GetOrStore("myKey", () => GetSomethingToCache());
public static class CacheExtensions
{
private static readonly object sync = new object();
private static TimeSpan defaultExpire = TimeSpan.FromMinutes(20);
public static T GetOrStore<T>(this Cache cache, string key, Func<T> generator) =>
cache.GetOrStore(key, generator, defaultExpire);
public static T GetOrStore<T>(this Cache cache, string key, Func<T> generator, TimeSpan expire)
{
var result = cache[key];
if (result == null)
{
lock (sync)
{
result = cache[key];
if (result == null)
{
result = generator();
cache.Insert(key, result, null, DateTime.UtcNow.AddMinutes(expire.TotalMinutes), Cache.NoSlidingExpiration);
}
}
}
return (T)result;
}
}
You can still use the cache (shared among all responses) and session (unique per user) for storage.
I like the following "try get from cache/create and store" pattern (c#-like pseudocode):
public static class CacheExtensions
{
public static T GetOrStore<T>(this Cache cache, string key, Func<T> generator)
{
var result = cache[key];
if(result == null)
{
result = generator();
cache[key] = result;
}
return (T)result;
}
}
you'd use this like so:
var user = HttpRuntime
.Cache
.GetOrStore<User>(
$"User{_userId}",
() => Repository.GetUser(_userId));
You can adapt this pattern to the Session, ViewState (ugh) or any other cache mechanism. You can also extend the ControllerContext.HttpContext (which I think is one of the wrappers in System.Web.Extensions), or create a new class to do it with some room for mocking the cache.
If you want it cached for the length of the request, put this in your controller base class:
public User User {
get {
User _user = ControllerContext.HttpContext.Items["user"] as User;
if (_user == null) {
_user = _repository.Get<User>(id);
ControllerContext.HttpContext.Items["user"] = _user;
}
return _user;
}
}
If you want to cache for longer, use the replace the ControllerContext call with one to Cache[]. If you do choose to use the Cache object to cache longer, you'll need to use a unique cache key as it will be shared across requests/users.
I took Will's answer and modified it to make the CacheExtensions
class static and to suggest a slight alteration in order to deal with the possibility of Func<T>
being null
:
public static class CacheExtensions
{
private static object sync = new object();
public const int DefaultCacheExpiration = 20;
/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
/// .Cache
/// .GetOrStore<User>(
/// string.Format("User{0}", _userId),
/// () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="cache">calling object</param>
/// <param name="key">Cache key</param>
/// <param name="generator">Func that returns the object to store in cache</param>
/// <returns></returns>
/// <remarks>Uses a default cache expiration period as defined in <see cref="CacheExtensions.DefaultCacheExpiration"/></remarks>
public static T GetOrStore<T>( this Cache cache, string key, Func<T> generator ) {
return cache.GetOrStore( key, (cache[key] == null && generator != null) ? generator() : default( T ), DefaultCacheExpiration );
}
/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
/// .Cache
/// .GetOrStore<User>(
/// string.Format("User{0}", _userId),
/// () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="cache">calling object</param>
/// <param name="key">Cache key</param>
/// <param name="generator">Func that returns the object to store in cache</param>
/// <param name="expireInMinutes">Time to expire cache in minutes</param>
/// <returns></returns>
public static T GetOrStore<T>( this Cache cache, string key, Func<T> generator, double expireInMinutes ) {
return cache.GetOrStore( key, (cache[key] == null && generator != null) ? generator() : default( T ), expireInMinutes );
}
/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
/// .Cache
/// .GetOrStore<User>(
/// string.Format("User{0}", _userId),_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="cache">calling object</param>
/// <param name="key">Cache key</param>
/// <param name="obj">Object to store in cache</param>
/// <returns></returns>
/// <remarks>Uses a default cache expiration period as defined in <see cref="CacheExtensions.DefaultCacheExpiration"/></remarks>
public static T GetOrStore<T>( this Cache cache, string key, T obj ) {
return cache.GetOrStore( key, obj, DefaultCacheExpiration );
}
/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
/// .Cache
/// .GetOrStore<User>(
/// string.Format("User{0}", _userId),
/// () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="cache">calling object</param>
/// <param name="key">Cache key</param>
/// <param name="obj">Object to store in cache</param>
/// <param name="expireInMinutes">Time to expire cache in minutes</param>
/// <returns></returns>
public static T GetOrStore<T>( this Cache cache, string key, T obj, double expireInMinutes ) {
var result = cache[key];
if ( result == null ) {
lock ( sync ) {
result = cache[key];
if ( result == null ) {
result = obj != null ? obj : default( T );
cache.Insert( key, result, null, DateTime.Now.AddMinutes( expireInMinutes ), Cache.NoSlidingExpiration );
}
}
}
return (T)result;
}
}
I would also consider taking this a step further to implement a testable Session solution that extends the System.Web.HttpSessionStateBase abstract class.
public static class SessionExtension
{
/// <summary>
///
/// </summary>
/// <example><![CDATA[
/// var user = HttpContext
/// .Session
/// .GetOrStore<User>(
/// string.Format("User{0}", _userId),
/// () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="cache"></param>
/// <param name="key"></param>
/// <param name="generator"></param>
/// <returns></returns>
public static T GetOrStore<T>( this HttpSessionStateBase session, string name, Func<T> generator ) {
var result = session[name];
if ( result != null )
return (T)result;
result = generator != null ? generator() : default( T );
session.Add( name, result );
return (T)result;
}
}