I\'d like to cache objects in ASP.NET MVC. I have a BaseController
that I want all Controllers to inherit from. In the BaseController there is a User
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.
@njappboy: Nice implementation. I would only defer the Generator( )
invocation until the last responsible moment. thus you can cache method invocations too.
/// <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, Generator, 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 )
{
var Result = Cache [ Key ];
if( Result == null )
{
lock( Sync )
{
if( Result == null )
{
Result = Generator( );
Cache.Insert( Key, Result, null, DateTime.Now.AddMinutes( ExpireInMinutes ), Cache.NoSlidingExpiration );
}
}
}
return ( T ) Result;
}
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.
A couple of the other answers here don't deal with the following:
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;
}
}
Implementation with a minimal cache locking. The value stored in the cache is wrapped in a container. If the value is not in the cache, then the value container is locked. The cache is locked only during the creation of the container.
public static class CacheExtensions
{
private static object sync = new object();
private class Container<T>
{
public T Value;
}
public static TValue GetOrStore<TValue>(this Cache cache, string key, Func<TValue> create, TimeSpan slidingExpiration)
{
return cache.GetOrStore(key, create, Cache.NoAbsoluteExpiration, slidingExpiration);
}
public static TValue GetOrStore<TValue>(this Cache cache, string key, Func<TValue> create, DateTime absoluteExpiration)
{
return cache.GetOrStore(key, create, absoluteExpiration, Cache.NoSlidingExpiration);
}
public static TValue GetOrStore<TValue>(this Cache cache, string key, Func<TValue> create, DateTime absoluteExpiration, TimeSpan slidingExpiration)
{
return cache.GetOrCreate(key, x => create());
}
public static TValue GetOrStore<TValue>(this Cache cache, string key, Func<string, TValue> create, DateTime absoluteExpiration, TimeSpan slidingExpiration)
{
var instance = cache.GetOrStoreContainer<TValue>(key, absoluteExpiration, slidingExpiration);
if (instance.Value == null)
lock (instance)
if (instance.Value == null)
instance.Value = create(key);
return instance.Value;
}
private static Container<TValue> GetOrStoreContainer<TValue>(this Cache cache, string key, DateTime absoluteExpiration, TimeSpan slidingExpiration)
{
var instance = cache[key];
if (instance == null)
lock (cache)
{
instance = cache[key];
if (instance == null)
{
instance = new Container<TValue>();
cache.Add(key, instance, null, absoluteExpiration, slidingExpiration, CacheItemPriority.Default, null);
}
}
return (Container<TValue>)instance;
}
}
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;
}
}