How can I cache objects in ASP.NET MVC?

asp.net MvcCaching

asp.net Mvc Problem Overview


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 property that will simply grab the User data from the database so that I can use it within the controller, or pass it to the views.

I'd like to cache this information. I'm using this information on every single page so there is no need to go to the database each page request.

I'd like something like:

if(_user is null)
  GrabFromDatabase
  StuffIntoCache
return CachedObject as User

How do I implement simple caching in ASP.NET MVC?

asp.net Mvc Solutions


Solution 1 - asp.net Mvc

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.

Solution 2 - asp.net Mvc

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;
    }

}

Solution 3 - asp.net Mvc

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.

Solution 4 - 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;
    }
}

Solution 5 - asp.net Mvc

I like to hide the fact that the data is cached in the repository. You can access the cache through the HttpContext.Current.Cache property and store the User information using "User"+id.ToString() as the key.

This means that all access to the User data from the repository will use cached data if available and requires no code changes in the model, controller, or view.

I have used this method to correct serious performance problems on a system that was querying the database for each User property and reduced page load times from minutes to single digit seconds.

Solution 6 - asp.net Mvc

@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;
}

Solution 7 - asp.net Mvc

If you don't need specific invalidation features of ASP.NET caching, static fields are pretty good, lightweight and easy to use. However, as soon as you needed the advanced features, you can switch to ASP.NET's Cache object for storage.

The approach I use is to create a property and a private field. If the field is null, the property will fill it and return it. I also provide an InvalidateCache method that manually sets the field to null. The advantage of this approach it that the caching mechanism is encapsulated in the property and you can switch to a different approach if you want.

Solution 8 - asp.net Mvc

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;
	}
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionrballView Question on Stackoverflow
Solution 1 - asp.net Mvcuser1228View Answer on Stackoverflow
Solution 2 - asp.net MvcnjappboyView Answer on Stackoverflow
Solution 3 - asp.net MvcJohn SheehanView Answer on Stackoverflow
Solution 4 - asp.net MvcDigitalDanView Answer on Stackoverflow
Solution 5 - asp.net MvcMatthewView Answer on Stackoverflow
Solution 6 - asp.net MvcSDReyesView Answer on Stackoverflow
Solution 7 - asp.net MvcmmxView Answer on Stackoverflow
Solution 8 - asp.net MvcIlya OrlovView Answer on Stackoverflow