16

I'm a bit confused on the proper usage of MemoryCache.

Should/can it be used to load static information to save on repeated calls? Should/can it be used to persist data on a view across several action methods?

I have an instance where I don't want to use the data store to populate and persist the data across my view. I started using the MemoryCache which works fine, however I'm starting to question if that was the correct approach.

My concerns was what happens if I have several users on the same page using the same MemoryCache?

John Doe
  • 3,053
  • 17
  • 48
  • 75

1 Answers1

36

First of all, MemoryCache is part of the System.Runtime.Caching namespace. It can be used by MVC applications, but it is not limited to MVC applications.

NOTE: There is also a System.Web.Caching namespace (much older) that can only be used with the ASP.NET framework (including MVC).


Should/can it be used to load static information to save on repeated calls?

Yes.

Should/can it be used to persist data on a view across several action methods?

Yes. If your views use the same data it can. Or if you have data that is on your _Layout.cshtml page that needs caching, it could be done in a global filter.

what happens if I have several users on the same page using the same MemoryCache?

Caching is shared between all users by default. It is specifically meant for keeping data in memory so it doesn't have to be fetched from the database on every request (for example, a list of state names on a checkout page for populating a dropdown for all users).

It is also a good idea to cache data that changes frequently for a second or two to prevent a flood of concurrent requests from being a denial-of-service attack on your database.

Caching depends on a unique key. It is possible to store individual user information in the cache by making the user's name or ID part of the key.

var key = "MyFavoriteItems-" + this.User.Identity.Name;

Warning: This method works only if you have a single web server. It won't scale to multiple web servers. Session state (which is meant for individual user memory storage) is a more scalable approach. However, session state is not always worth the tradeoffs.


Typical Caching Pattern

Note that although MemoryCache is thread-safe, using it in conjunction with a database call can make the operation cross threads. Without locking, it is possible that you might get several queries to the database to reload the cache when it expires.

So, you should use a double-checked locking pattern to ensure only one thread makes it through to reload the cache from the database.

Let's say you have a list that is wasteful to get on every request because every user will need the list when they get to a specific page.

public IEnumerable<StateOrProvinceDto> GetStateOrProvinceList(string country)
{
    // Query the database to get the data...
}

To cache the result of this query, you can add another method with a double-checked locking pattern and use it to call your original method.

NOTE: A common approach is to use the decorator pattern to make the caching seamless to your API.

private ObjectCache _cache = MemoryCache.Default;
private object _lock = new object();

// NOTE: The country parameter would typically be a database key type,
// (normally int or Guid) but you could still use it to build a unique key using `.ToString()`.
public IEnumerable<StateOrProvinceDto> GetCachedStateOrProvinceList(string country)
{
    // Key can be any string, but it must be both 
    // unique across the cache and deterministic
    // for this function.
    var key = "GetCachedStateList" + country;

    // Try to get the object from the cache
    var stateOrProvinceList = _cache[key] as IEnumerable<StateOrProvinceDto>;

    // Check whether the value exists
    if (stateOrProvinceList == null)
    {
        lock (_lock)
        {
            // Try to get the object from the cache again
           stateOrProvinceList = _cache[key] as IEnumerable<StateOrProvinceDto>;

            // Double-check that another thread did 
            // not call the DB already and load the cache
            if (stateOrProvinceList == null)
            {
                // Get the list from the DB
                stateOrProvinceList = GetStateOrProvinceList()

                // Add the list to the cache
                _cache.Set(key, stateOrProvinceList, DateTimeOffset.Now.AddMinutes(5));
            }
        }
    }

    // Return the cached list
    return stateOrProvinceList;
}

So, you call the GetCachedStateOrProvinceList and it will automatically get the list from the cache and if it is not cached will automatically load the list from the database into the cache. Only 1 thread will be allowed to call the database, the rest will wait until the cache is populated and then return the list from the cache once available.

Note also that the list of states or provinces for each country will be cached individually.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • 1
    "Should/can it be used to persist data on a view across several action methods?" actually the answer to that is no. There is no guarantee for how long cache would persist the data. Even if it set not to expire, cache manager can decide to discard the cached data before the next request executes, due to a low memory condition. To preserve requests between multiple actions use `HttpContext.Items`. – Sedat Kapanoglu Apr 17 '16 at 17:11
  • Ty for he wealth of information. In my case I am trying to prevent rehydrating the data. This is due to the fact that I have to remote to a sever pull the file and break that file into objects. I'll have to rethink my problem better. – John Doe Apr 18 '16 at 14:45
  • I see. Well, the `HttpContext.Items` dictionary that Sedat mentioned in his comment is a cache that applies only to the current request. Helpful if you have multiple parts of the application using the same data and don't want to fetch it from the database multiple times. – NightOwl888 Apr 18 '16 at 15:24
  • @NightOwl888 Any benefit over a ConcurrentDictionary (which seems to be threadsafe and strongly-typed)? – Natalie Perret Nov 23 '17 at 15:27
  • @Kerry - `ConcurrentDictionary` is a lower-level component. It can be used to create a cache, but it does not contain any high level caching features such as item expiration or cache dependencies. In other words, it doesn't have any concept of item lifetime. Once you put an item in a `ConcurrentDictionary` it lives for as long as the `ConcurrentDictionary` lives or until you manually remove it. A cache can evict an item based on a fixed or sliding time limit and/or an edit to a file or database dependency or some other custom rule. – NightOwl888 Nov 04 '20 at 21:38