260

I have read lots of information about page caching and partial page caching in a MVC application. However, I would like to know how you would cache data.

In my scenario I will be using LINQ to Entities (entity framework). On the first call to GetNames (or whatever the method is) I want to grab the data from the database. I want to save the results in cache and on the second call to use the cached version if it exists.

Can anyone show an example of how this would work, where this should be implemented (model?) and if it would work.

I have seen this done in traditional ASP.NET apps , typically for very static data.

Coolcoder
  • 4,036
  • 6
  • 28
  • 35
  • 1
    In reviewing the answers below, be sure to consider whether you want to have your controller have knowledge of / responsibility for data access and caching concerns. Generally you want to separate this. See the Repository Pattern for a good way to do so: http://deviq.com/repository-pattern/ – ssmith Jul 14 '16 at 17:35

14 Answers14

410

Here's a nice and simple cache helper class/service I use:

using System.Runtime.Caching;  

public class InMemoryCache: ICacheService
{
    public T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class
    {
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(10));
        }
        return item;
    }
}

interface ICacheService
{
    T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class;
}

Usage:

cacheProvider.GetOrSet("cache key", (delegate method if cache is empty));

Cache provider will check if there's anything by the name of "cache id" in the cache, and if there's not, it will call a delegate method to fetch data and store it in cache.

Example:

var products=cacheService.GetOrSet("catalog.products", ()=>productRepository.GetAll())
Appulus
  • 18,630
  • 11
  • 38
  • 46
Hrvoje Hudo
  • 8,994
  • 5
  • 33
  • 42
  • Is there a way to pass parameters to the callback? – Todd Smith Dec 08 '08 at 18:16
  • What CacheService are you referring to? Can you please provide a more complete example. – Coolcoder Dec 11 '08 at 14:24
  • Coolcoder: there's not much more too it, that's almost all the code. – Anthony Dec 16 '08 at 14:58
  • Here I use system.web.caching, but caching object can be injected in constructor with ioc/di, you then you could have disk caching, probably velocity/memcache, etc. – Hrvoje Hudo Jun 09 '09 at 07:52
  • 3
    I've adapted this so that the caching mechanism is used per user session by using HttpContext.Current.Session instead. I've also put a Cache property on my BaseController class so its easy access and updated the constructor allow for DI for unit testing. Hope this helps. – WestDiscGolf Jan 08 '10 at 12:23
  • @Todd Smith: Change the signature of the Get method to receive a Func instead. – murki Jan 08 '10 at 23:37
  • 1
    You can also make this class and method static for reusability among other controllers. – Alex Mar 09 '10 at 19:45
  • 5
    This class shouldn't depend on HttpContext. I simplified it just for example purpose here. Cache object must be inserted through constructor - it can be replaced then with other caching mechanisms . All this is achieved with IoC/DI, along with static (singleton) life cycle. – Hrvoje Hudo Jul 26 '10 at 11:12
  • 1
    This is a basic setup that will work. There are only two things I don't like about it: it requires the code know about both the repository and the caching and it also requires that you know the cache key when asking for the data. This means that you run the risk of having two keys for the same data. Also, it is a pain to look up the key. The caching should be baked in and automated somewhere. – Brendan Enrick Jun 03 '11 at 20:56
  • 3
    @Brendan - and worse still, it has magic strings in place for the cache keys, rather than inferring them from the method name and parameters. – ssmith Jun 03 '11 at 20:58
  • 1
    Question: is there a reason this uses *HttpRuntime*.Cache to find an item and *HttpContext.Current*.Cache to set it? – pettys Nov 09 '12 at 23:00
  • @pettys true, that is mistake, even it would work because both objects are just references to system.web.caching.cache. Also, it would be good to extract that and access it through interface, for better unit testing and decoupling – Hrvoje Hudo Nov 13 '12 at 19:46
  • @Hrvoje thanks - just wanted to know if it was a mistake or something brilliant I wasn't aware of :) – pettys Nov 14 '12 at 15:42
  • 5
    This is an awesome low level solution. Like others have alluded to, you'd want to wrap this in a type safe, domain-specific class. Accessing this directly in your controllers would be a maintenance nightmare because of the magic strings. – Josh Noe Aug 01 '13 at 16:15
  • @Hrvoje thanks for sharing that ! I don't understand how you manage the expiration date ? Will your cache growth indefinitely ? It should be interesting to add a parameter to define the expiration date. – Michael Alves Feb 03 '15 at 10:29
  • 1
    @MichaelAlves just modified implementation a little bit, and added usage of MemoryCache (available in .NET 4.5) where absolute expiration date can be specified. – Hrvoje Hudo Feb 04 '15 at 11:27
  • 1
    @JoshNoe of course, in real world scenario you should use enums or string consts for cache keys. Also, depending in app complexity, it can be used from controller (simple apps), or from dedicated caching layer that could sit on top of DAL/Repository level. Complex apps can have several caching layers, from HTTP down to ORM first/second level cache. – Hrvoje Hudo Feb 04 '15 at 11:32
  • 1
    My favorite thing about delegates is that you don't even have to pass new wrapper delegates when the signature of the method being wrapped is the same as the delegate signature. In your example, this would be the same: `var products=cacheService.GetOrSet("catalog.products", productRepository.GetAll)`, making the syntax just that shorter and cleaner. – smdrager Feb 08 '15 at 17:42
  • @HrvojeHudo, I love the code. I would add a lock that depend on cache key. – A Khudairy Mar 12 '15 at 20:11
  • Excellent! What would a moq of GetOrSet look like? Getting wrapped up around the getItemCallback – gnome May 09 '16 at 14:46
  • I can't believe this gets so many up votes. You never want to create a data cache that exists as a SECOND source of data. Doing that allows your db and your cache to get out of sync. If caching, always wrap calls to the db in with cache checks/updates. The pattern shown here allows the user to check the cache and presumably call the db INDEPENDENTELY if the cache fails. This pattern allows access to db methods AROUND the cache which leads to mismatches. The correct approach is shown by @terjetyl. –  Jan 05 '18 at 21:03
  • @sam cache always exists as the second source of data, you can't go around that. I still prefer my approach (even though in the real app it looks different, but the cache interface with callback is the same), because terjetyl DAL method is not orthogonal to cache provider. To prevent out of sync issues, which are possible of course, proper OO design and layers needs to be used, and this simple cache service can be used inside terjetyl GetNames method, where you inject ICacheService, you shouldn't go directly to Cache object. – Hrvoje Hudo Jan 07 '18 at 18:36
  • I wonder why do we need `where T : class` here. I think this caching can work without that as well. Use (casting) instead of `as` operator. – Al Kepp Jan 19 '18 at 11:55
  • 1
    I recommend implementing such a wrapper using [GetOrCreate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.getorcreate) rather than `Get`. That said, I don't see much benefit to this approach over just directly calling `MemoryCache.Default.AddOrGetExisting` (you can still use DI to inject an `MemoryCache.Default` as an `IMemoryCache`, so you won't be losing flexibility). – Brian Aug 27 '18 at 21:27
  • @Hrvoje Hudo,How to consume this cache records from cshtml or html page – Deepan Raj Apr 10 '19 at 06:59
  • @deepan-raj you map it to the model you're sending to your View (M in MVC), or use it as a view model – Hrvoje Hudo Apr 11 '19 at 08:08
79

Reference the System.Web dll in your model and use System.Web.Caching.Cache

    public string[] GetNames()
    {
      string[] names = Cache["names"] as string[];
      if(names == null) //not in cache
      {
        names = DB.GetNames();
        Cache["names"] = names;
      }
      return names;
    }

A bit simplified but I guess that would work. This is not MVC specific and I have always used this method for caching data.

DanB
  • 2,022
  • 1
  • 12
  • 24
terjetyl
  • 9,497
  • 4
  • 54
  • 72
  • 91
    I don't recommend this solution: in the return, you might get a null object again, because it's re-reading in the cache and it might have been dropped from the cache already. I'd rather do: public string[] GetNames() { string[] noms = Cache["names"]; if(noms == null) { noms = DB.GetNames(); Cache["names"] = noms; } return (noms); } – Oli Jul 08 '09 at 15:30
  • I agree with Oli.. getting the results from the actual call to the DB is better than getting them from the cache – CodeClimber Jul 08 '09 at 21:48
  • 1
    Does this work with the `DB.GetNames().AsQueryable` method of delaying the query? – Chase Florell Aug 09 '10 at 21:56
  • Not unless you change return value from string[] to IEnumerable – terjetyl Aug 10 '10 at 08:55
  • 14
    If you don't set expiration..when does cache expire by default? – Chaka Sep 27 '13 at 13:58
  • Couldn't you do something like `if ((names = Cache["names"]) == null) { . . .`? – BenR Nov 17 '14 at 22:39
  • @BenRecord, sure, but people will tell you that it’s less readable than the two-line equivalent of `var names = Cache["names"] as string[]; if (names == null) {…`. @Chaka [the docs](https://msdn.microsoft.com/en-us/library/6hbbsfk6%28v=vs.100%29.aspx) say “For example, when system memory becomes scarce, the cache automatically removes seldom-used or low-priority items to free memory.”, so it’s safest to treat the cache as if it’s possible that setting a key in it never actually stores anything (though I don’t know if that ever actually happens). – binki Jan 27 '15 at 16:02
  • Could it be like `. . . Cache["names"] = names = DB.GetNames(); . . .`? – stenlytw Apr 26 '16 at 17:29
43

I'm referring to TT's post and suggest the following approach:

Reference the System.Web dll in your model and use System.Web.Caching.Cache

public string[] GetNames()
{ 
    var noms = Cache["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        Cache["names"] = noms; 
    }

    return ((string[])noms);
}

You should not return a value re-read from the cache, since you'll never know if at that specific moment it is still in the cache. Even if you inserted it in the statement before, it might already be gone or has never been added to the cache - you just don't know.

So you add the data read from the database and return it directly, not re-reading from the cache.

Liam
  • 27,717
  • 28
  • 128
  • 190
Oli
  • 1,762
  • 3
  • 19
  • 22
  • But doesn't the line `Cache["names"] = noms;` put in the cache? – Omar Jan 19 '10 at 17:16
  • 2
    @Baddie Yes it does. But this example is different to the first Oli is referring to, because he doesn't access the cache again - the problem is that just doing: return (string[])Cache["names"]; .. COULD result in a null value being returned, because it COULD have expired. It's not likely, but it can happen. This example is better, because we store the actual value returned from the db in memory, cache that value, and then return that value, not the value re-read from the cache. – jamiebarrow Nov 02 '10 at 16:19
  • Or... the value re-read from cache, if it still exists (!= null). Hence, the entire point of caching. This is just to say that it double-checks for null values, and reads the database where necessary. Very smart, thanks Oli! – Sean Kendle Nov 23 '15 at 20:33
42

For .NET 4.5+ framework

add reference: System.Runtime.Caching

add using statement: using System.Runtime.Caching;

public string[] GetNames()
{ 
    var noms = System.Runtime.Caching.MemoryCache.Default["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        System.Runtime.Caching.MemoryCache.Default["names"] = noms; 
    }

    return ((string[])noms);
}

In the .NET Framework 3.5 and earlier versions, ASP.NET provided an in-memory cache implementation in the System.Web.Caching namespace. In previous versions of the .NET Framework, caching was available only in the System.Web namespace and therefore required a dependency on ASP.NET classes. In the .NET Framework 4, the System.Runtime.Caching namespace contains APIs that are designed for both Web and non-Web applications.

More info:

juFo
  • 17,849
  • 10
  • 105
  • 142
26

Steve Smith did two great blog posts which demonstrate how to use his CachedRepository pattern in ASP.NET MVC. It uses the repository pattern effectively and allows you to get caching without having to change your existing code.

http://ardalis.com/Introducing-the-CachedRepository-Pattern

http://ardalis.com/building-a-cachedrepository-via-strategy-pattern

In these two posts he shows you how to set up this pattern and also explains why it is useful. By using this pattern you get caching without your existing code seeing any of the caching logic. Essentially you use the cached repository as if it were any other repository.

Henry C
  • 4,781
  • 4
  • 43
  • 83
Brendan Enrick
  • 4,277
  • 2
  • 26
  • 40
  • Links dead as of 2013-08-31. – CBono Aug 31 '13 at 15:51
  • 3
    Content has moved now. http://ardalis.com/Introducing-the-CachedRepository-Pattern and http://ardalis.com/building-a-cachedrepository-via-strategy-pattern – Uchitha Jan 19 '15 at 08:17
8

I have used it in this way and it works for me. https://msdn.microsoft.com/en-us/library/system.web.caching.cache.add(v=vs.110).aspx parameters info for system.web.caching.cache.add.

public string GetInfo()
{
     string name = string.Empty;
     if(System.Web.HttpContext.Current.Cache["KeyName"] == null)
     {
         name = GetNameMethod();
         System.Web.HttpContext.Current.Cache.Add("KeyName", name, null, DateTime.Noew.AddMinutes(5), Cache.NoSlidingExpiration, CacheitemPriority.AboveNormal, null);
     }
     else
     {
         name = System.Web.HttpContext.Current.Cache["KeyName"] as string;
     }

      return name;

}
user3776645
  • 397
  • 3
  • 5
4

AppFabric Caching is distributed and an in-memory caching technic that stores data in key-value pairs using physical memory across multiple servers. AppFabric provides performance and scalability improvements for .NET Framework applications. Concepts and Architecture

Arun Duth
  • 49
  • 1
  • 1
3

Extending @Hrvoje Hudo's answer...

Code:

using System;
using System.Runtime.Caching;

public class InMemoryCache : ICacheService
{
    public TValue Get<TValue>(string cacheKey, int durationInMinutes, Func<TValue> getItemCallback) where TValue : class
    {
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }

    public TValue Get<TValue, TId>(string cacheKeyFormat, TId id, int durationInMinutes, Func<TId, TValue> getItemCallback) where TValue : class
    {
        string cacheKey = string.Format(cacheKeyFormat, id);
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback(id);
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }
}

interface ICacheService
{
    TValue Get<TValue>(string cacheKey, Func<TValue> getItemCallback) where TValue : class;
    TValue Get<TValue, TId>(string cacheKeyFormat, TId id, Func<TId, TValue> getItemCallback) where TValue : class;
}

Examples

Single item caching (when each item is cached based on its ID because caching the entire catalog for the item type would be too intensive).

Product product = cache.Get("product_{0}", productId, 10, productData.getProductById);

Caching all of something

IEnumerable<Categories> categories = cache.Get("categories", 20, categoryData.getCategories);

Why TId

The second helper is especially nice because most data keys are not composite. Additional methods could be added if you use composite keys often. In this way you avoid doing all sorts of string concatenation or string.Formats to get the key to pass to the cache helper. It also makes passing the data access method easier because you don't have to pass the ID into the wrapper method... the whole thing becomes very terse and consistant for the majority of use cases.

smdrager
  • 7,327
  • 6
  • 39
  • 49
3

Here's an improvement to Hrvoje Hudo's answer. This implementation has a couple of key improvements:

  • Cache keys are created automatically based on the function to update data and the object passed in that specifies dependencies
  • Pass in time span for any cache duration
  • Uses a lock for thread safety

Note that this has a dependency on Newtonsoft.Json to serialize the dependsOn object, but that can be easily swapped out for any other serialization method.

ICache.cs

public interface ICache
{
    T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class;
}

InMemoryCache.cs

using System;
using System.Reflection;
using System.Runtime.Caching;
using Newtonsoft.Json;

public class InMemoryCache : ICache
{
    private static readonly object CacheLockObject = new object();

    public T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class
    {
        string cacheKey = GetCacheKey(getItemCallback, dependsOn);
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            lock (CacheLockObject)
            {
                item = getItemCallback();
                MemoryCache.Default.Add(cacheKey, item, DateTime.Now.Add(duration));
            }
        }
        return item;
    }

    private string GetCacheKey<T>(Func<T> itemCallback, object dependsOn) where T: class
    {
        var serializedDependants = JsonConvert.SerializeObject(dependsOn);
        var methodType = itemCallback.GetType();
        return methodType.FullName + serializedDependants;
    }
}

Usage:

var order = _cache.GetOrSet(
    () => _session.Set<Order>().SingleOrDefault(o => o.Id == orderId)
    , new { id = orderId }
    , new TimeSpan(0, 10, 0)
);
DShook
  • 14,833
  • 9
  • 45
  • 55
  • 2
    The `if (item == null)` should be inside the lock. Now when this `if` is before the lock, race condition can occur. Or even better, you should keep `if` before the lock, but recheck if cache is still empty as the first line inside the lock. Because if two threads come at the same time, they both update the cache. Your current lock is not helpful. – Al Kepp Jan 19 '18 at 11:51
3
public sealed class CacheManager
{
    private static volatile CacheManager instance;
    private static object syncRoot = new Object();
    private ObjectCache cache = null;
    private CacheItemPolicy defaultCacheItemPolicy = null;

    private CacheEntryRemovedCallback callback = null;
    private bool allowCache = true;

    private CacheManager()
    {
        cache = MemoryCache.Default;
        callback = new CacheEntryRemovedCallback(this.CachedItemRemovedCallback);

        defaultCacheItemPolicy = new CacheItemPolicy();
        defaultCacheItemPolicy.AbsoluteExpiration = DateTime.Now.AddHours(1.0);
        defaultCacheItemPolicy.RemovedCallback = callback;
        allowCache = StringUtils.Str2Bool(ConfigurationManager.AppSettings["AllowCache"]); ;
    }
    public static CacheManager Instance
    {
        get
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                    {
                        instance = new CacheManager();
                    }
                }
            }

            return instance;
        }
    }

    public IEnumerable GetCache(String Key)
    {
        if (Key == null || !allowCache)
        {
            return null;
        }

        try
        {
            String Key_ = Key;
            if (cache.Contains(Key_))
            {
                return (IEnumerable)cache.Get(Key_);
            }
            else
            {
                return null;
            }
        }
        catch (Exception)
        {
            return null;
        }
    }

    public void ClearCache(string key)
    {
        AddCache(key, null);
    }

    public bool AddCache(String Key, IEnumerable data, CacheItemPolicy cacheItemPolicy = null)
    {
        if (!allowCache) return true;
        try
        {
            if (Key == null)
            {
                return false;
            }

            if (cacheItemPolicy == null)
            {
                cacheItemPolicy = defaultCacheItemPolicy;
            }

            String Key_ = Key;

            lock (Key_)
            {
                return cache.Add(Key_, data, cacheItemPolicy);
            }
        }
        catch (Exception)
        {
            return false;
        }
    }

    private void CachedItemRemovedCallback(CacheEntryRemovedArguments arguments)
    {
        String strLog = String.Concat("Reason: ", arguments.RemovedReason.ToString(), " | Key-Name: ", arguments.CacheItem.Key, " | Value-Object: ", arguments.CacheItem.Value.ToString());
        LogManager.Instance.Info(strLog);
    }
}
Chau
  • 31
  • 2
1

I use two classes. First one the cache core object:

public class Cacher<TValue>
    where TValue : class
{
    #region Properties
    private Func<TValue> _init;
    public string Key { get; private set; }
    public TValue Value
    {
        get
        {
            var item = HttpRuntime.Cache.Get(Key) as TValue;
            if (item == null)
            {
                item = _init();
                HttpContext.Current.Cache.Insert(Key, item);
            }
            return item;
        }
    }
    #endregion

    #region Constructor
    public Cacher(string key, Func<TValue> init)
    {
        Key = key;
        _init = init;
    }
    #endregion

    #region Methods
    public void Refresh()
    {
        HttpRuntime.Cache.Remove(Key);
    }
    #endregion
}

Second one is list of cache objects:

public static class Caches
{
    static Caches()
    {
        Languages = new Cacher<IEnumerable<Language>>("Languages", () =>
                                                          {
                                                              using (var context = new WordsContext())
                                                              {
                                                                  return context.Languages.ToList();
                                                              }
                                                          });
    }
    public static Cacher<IEnumerable<Language>> Languages { get; private set; }
}
Berezh
  • 942
  • 9
  • 12
0

I will say implementing Singleton on this persisting data issue can be a solution for this matter in case you find previous solutions much complicated

 public class GPDataDictionary
{
    private Dictionary<string, object> configDictionary = new Dictionary<string, object>();

    /// <summary>
    /// Configuration values dictionary
    /// </summary>
    public Dictionary<string, object> ConfigDictionary
    {
        get { return configDictionary; }
    }

    private static GPDataDictionary instance;
    public static GPDataDictionary Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new GPDataDictionary();
            }
            return instance;
        }
    }

    // private constructor
    private GPDataDictionary() { }

}  // singleton
GeraGamo
  • 226
  • 3
  • 6
0
HttpContext.Current.Cache.Insert("subjectlist", subjectlist);
Herr Kater
  • 3,242
  • 2
  • 22
  • 33
-8

You can also try and use the caching built into ASP MVC:

Add the following attribute to the controller method you'd like to cache:

[OutputCache(Duration=10)]

In this case the ActionResult of this will be cached for 10 seconds.

More on this here

Chris James
  • 11,571
  • 11
  • 61
  • 89