136

I assume this code has concurrency issues:

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        expensiveString = MemoryCache.Default[CacheKey] as string;
    }
    else
    {
        CacheItemPolicy cip = new CacheItemPolicy()
        {
            AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
        };
        expensiveString = SomeHeavyAndExpensiveCalculation();
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
    }
    return expensiveString;
}

The reason for the concurrency issue is that multiple threads can get a null key and then attempt to insert data into cache.

What would be the shortest and cleanest way to make this code concurrency proof? I like to follow a good pattern across my cache related code. A link to an online article would be a great help.

UPDATE:

I came up with this code based on @Scott Chamberlain's answer. Can anyone find any performance or concurrency issue with this? If this works, it would save many line of code and errors.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
        }

        private static string SomeHeavyAndExpensiveXYZCalculation() {return "Expensive";}
        private static string SomeHeavyAndExpensiveABCCalculation() {return "Expensive";}

        public static class MemoryCacheHelper
        {
            public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, Func<T> GetData)
                where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                {
                    return cachedData;
                }

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                    {
                        return cachedData;
                    }

                    //The value still did not exist so we now write it in to the cache.
                    CacheItemPolicy cip = new CacheItemPolicy()
                    {
                        AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
                    };
                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, cip);
                    return cachedData;
                }
            }
        }
    }
}
Allan Xu
  • 7,998
  • 11
  • 51
  • 122
  • 3
    why dont u use `ReaderWriterLockSlim` ? – DarthVader Jan 21 '14 at 21:23
  • 2
    I agree with DarthVader... I would think you lean `ReaderWriterLockSlim`... But I would also use [this](http://artisansoftware.blogspot.com/2013/12/using-statements-with-proxies.html) technique to avoid `try-finally` statements. – poy Jan 21 '14 at 22:51
  • 2
    For your updated version, I would not lock on a single cacheLock anymore, I would lock per key instead. This can be easily done with a `Dictionary` where the key is the same key you use in your `MemoryCache` and the object in the dictionary is just a basic `Object` you lock on. However, that being said, I would reccomend you read through Jon Hanna's answer. Without proper profileing you may be slowing down your program more with locking than lettings two instances of `SomeHeavyAndExpensiveCalculation()` run and have one result thrown away. – Scott Chamberlain Jan 22 '14 at 06:11
  • @scott it does not lock on a single cacheLock. It is responsibility of the caller to send an object along with cacheKey. At the same time your Dictionary approach makes it more automated. – Allan Xu Jan 22 '14 at 15:23
  • Ah, I did not notice you are passing in the lock object. I would use `ConcurrentDictionary` instead of `Dictionary` so you don't need to use any locks on it, it would just take one line of code [`var cacheLock = lockDictionary.GetOrAdd(cacheKey, new Object());`](http://msdn.microsoft.com/en-us/library/ee378674%28v=vs.110%29.aspx) – Scott Chamberlain Jan 22 '14 at 15:24
  • 1
    It seems to me that creating the CacheItemPolicy after getting the expensive value to cache would be more accurate. In a worst case scenario such as creating a summary report that takes 21 minutes to return the "expensive string" (maybe containing filename of PDF report) would already be "expired" before it was returned. – Wonderbird Aug 28 '14 at 05:25
  • 1
    @Wonderbird Good point, I updated my answer to do that. – Scott Chamberlain Oct 01 '14 at 14:07
  • 1
    "I find your lock of graith... disturbing." -Darth Vader – Timo Oct 26 '15 at 13:53

10 Answers10

106

This is my 2nd iteration of the code. Because MemoryCache is thread safe you don't need to lock on the initial read, you can just read and if the cache returns null then do the lock check to see if you need to create the string. It greatly simplifies the code.

const string CacheKey = "CacheKey";
static readonly object cacheLock = new object();
private static string GetCachedData()
{

    //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

    if (cachedString != null)
    {
        return cachedString;
    }

    lock (cacheLock)
    {
        //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
        cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The value still did not exist so we now write it in to the cache.
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        CacheItemPolicy cip = new CacheItemPolicy()
                              {
                                  AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
                              };
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
        return expensiveString;
    }
}

EDIT: The below code is unnecessary but I wanted to leave it to show the original method. It may be useful to future visitors who are using a different collection that has thread safe reads but non-thread safe writes (almost all of classes under the System.Collections namespace is like that).

Here is how I would do it using ReaderWriterLockSlim to protect access. You need to do a kind of "Double Checked Locking" to see if anyone else created the cached item while we where waiting to to take the lock.

const string CacheKey = "CacheKey";
static readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
static string GetCachedData()
{
    //First we do a read lock to see if it already exists, this allows multiple readers at the same time.
    cacheLock.EnterReadLock();
    try
    {
        //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }
    }
    finally
    {
        cacheLock.ExitReadLock();
    }

    //Only one UpgradeableReadLock can exist at one time, but it can co-exist with many ReadLocks
    cacheLock.EnterUpgradeableReadLock();
    try
    {
        //We need to check again to see if the string was created while we where waiting to enter the EnterUpgradeableReadLock
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The entry still does not exist so we need to create it and enter the write lock
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        cacheLock.EnterWriteLock(); //This will block till all the Readers flush.
        try
        {
            CacheItemPolicy cip = new CacheItemPolicy()
            {
                AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
            };
            MemoryCache.Default.Set(CacheKey, expensiveString, cip);
            return expensiveString;
        }
        finally 
        {
            cacheLock.ExitWriteLock();
        }
    }
    finally
    {
        cacheLock.ExitUpgradeableReadLock();
    }
}
Pawel Maga
  • 5,428
  • 3
  • 38
  • 62
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • 1
    @DarthVader in what way will the above code not work? also this is not strictly "double checked locking" I am just following a similar pattern and it was the best way I could think of to describe it. That is why I said it was ***a kind of*** double checked locking. – Scott Chamberlain Jan 21 '14 at 21:41
  • I didnt comment on your code. I was commenting that Double Check Locking Doesnt work. Your code is fine. – DarthVader Jan 21 '14 at 21:48
  • 1
    I find it hard to see what situations this sort of locking and this sort of storage would make sense in though: If you're locking on all creations of values going into a `MemoryCache` chances are at least one of those two things was wrong. – Jon Hanna Jan 21 '14 at 22:44
  • @ScottChamberlain just looking at this code, and isn't it susceptible to an exception being thrown between the acquisition of the lock and the try block. The author of C# In a Nutshell discusses this here, http://www.albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit – BrutalSimplicity May 12 '18 at 15:27
  • Yes it could, however the kind of exceptions that could cause that to happen would likely be very bad situations I would want the program to tear down the AppDomain, so that's why I did not do it. – Scott Chamberlain May 12 '18 at 15:32
  • 11
    A downside of this code is that CacheKey "A" will block a request to CacheKey "B" if both are not cached yet. To solve this you could use a concurrentDictionary in which you store the cachekeys to lock on – MichaelD Jun 27 '18 at 06:18
  • Do you mean that for collections with non thread safe writes you'd have to lock on reads as well because if another thread is currently writing anything to the collection, the collection isn't stable at this moment (e.g. the write is causing re-balancing of a tree) and no read should be allowed when the collection isn't stable, therefore the 1st implementation you wrote should be used to prevent any reads when a write is in progress. Is this correct? – BornToCode Sep 02 '21 at 21:39
  • 1
    Correct. The two safe states is unlimited readers and no writers or a single writer and no readers. ReaderWriterLock follows that pattern. – Scott Chamberlain Sep 04 '21 at 01:22
  • Is the reason you use UpgradeableReadLock only to allow reads while getting the current value from MemoryCache? I expect `MemoryCache.Default.Get(CacheKey, null)` to be very quick so should I even care about not blocking other reads while making that quick action? (For example [here](https://stackoverflow.com/a/26578074/1057791) it states that if "the write-condition check is super fast" there's no need for UpgradeableReadLock) – BornToCode Sep 09 '21 at 23:36
52

There is an open source library [disclaimer: that I wrote]: LazyCache that IMO covers your requirement with two lines of code:

IAppCache cache = new CachingService();
var cachedResults = cache.GetOrAdd("CacheKey", 
  () => SomeHeavyAndExpensiveCalculation());

It has built in locking by default so the cacheable method will only execute once per cache miss, and it uses a lambda so you can do "get or add" in one go. It defaults to 20 minutes sliding expiration.

There's even a NuGet package ;)

Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
alastairtree
  • 3,960
  • 32
  • 49
  • 5
    The Dapper of caching. – Charles Burns Dec 12 '16 at 23:50
  • 4
    This enables me to be a lazy developer which makes this the best answer! – jdnew18 Nov 15 '17 at 15:22
  • Worth mentioning the article that the github page for LazyCache points to is quite a good read for the reasons behind it. https://alastaircrabtree.com/the-easy-way-to-add-caching-to-net-application-and-make-it-faster-is-called-lazycache/ – Rafael Merlin Jul 11 '19 at 13:34
  • 2
    Does it lock per key or per cache? – jjxtra Dec 19 '19 at 14:12
  • It's slightly more complicated, it's per cache to insert into a dictionary but the lazy will lock per key to evaluate the cachable delegate. – alastairtree Dec 19 '19 at 15:35
  • Hi @alastairtree, thanks for what seems like a great library! just to be sure - if creating the object on key "SlowKey" is taking 10 minutes, "FastKey" can just be created in the mean time - it is not getting blocked somewhere until "SlowKey" is done? – Dirk Boer Feb 08 '20 at 16:48
  • 1
    @DirkBoer no it won't get blocked because of the way the locks and lazy are used in lazycache – alastairtree Feb 08 '20 at 17:34
32

I've solved this issue by making use of the AddOrGetExisting method on the MemoryCache and the use of Lazy initialization.

Essentially, my code looks something like this:

static string GetCachedData(string key, DateTimeOffset offset)
{
    Lazy<String> lazyObject = new Lazy<String>(() => SomeHeavyAndExpensiveCalculationThatReturnsAString());
    var returnedLazyObject = MemoryCache.Default.AddOrGetExisting(key, lazyObject, offset); 
    if (returnedLazyObject == null)
       return lazyObject.Value;
    return ((Lazy<String>) returnedLazyObject).Value;
}

Worst case scenario here is that you create the same Lazy object twice. But that is pretty trivial. The use of AddOrGetExisting guarantees that you'll only ever get one instance of the Lazy object, and so you're also guaranteed to only call the expensive initialization method once.

Keith
  • 706
  • 5
  • 13
  • 4
    The problem with this type of approach is that you can insert invalid data. If `SomeHeavyAndExpensiveCalculationThatResultsAString()` threw an exception, it's stuck in the cache. Even transient exceptions will get cached with `Lazy`: http://msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx – Scott Wegner Jan 24 '14 at 16:19
  • 2
    While its true that Lazy can return an error if the initialization exception fails, that is a pretty easy thing to detect. You can then evict any Lazy that resolves to an error from the cache, create a new Lazy, put that in the cache, and resolve it. In our own code, we do something similar. We retry a set number of times before we throw an error. – Keith Jan 24 '14 at 16:57
  • 12
    AddOrGetExisting return null if the item was not present, so you should check and return lazyObject in that case – Gian Marco Aug 11 '14 at 13:56
  • 1
    Using LazyThreadSafetyMode.PublicationOnly will avoid the caching of exceptions. – Clement Jan 23 '15 at 00:49
  • 2
    According to the comments in [this blog post](http://blog.falafel.com/working-system-runtime-caching-memorycache/) if it's extremely expensive to initialize the cache entry, it's better to just evict on an exception (as shown in the example in the blog post) rather than use PublicationOnly, because there is a possibility that all the threads can call the initializer at the same time. – bcr Jul 31 '15 at 17:37
  • stupid question but how to use `LazyThreadSafetyMode.PublicationOnly` – Ali Umair Sep 17 '18 at 10:19
  • 1
    `You can then evict any Lazy that resolves to an error from the cache, create a new Lazy, put that in the cache, and resolve it` Alternatively, consider using `LazyWithNoExceptionCaching` - https://stackoverflow.com/a/42567351/34092 to avoid the need to do that. – mjwills Sep 21 '18 at 00:14
  • LazyCache has logic to handle the scenario of inadvertently caching an exception again saving you a few more lines of code – alastairtree May 29 '19 at 08:17
18

I assume this code has concurrency issues:

Actually, it's quite possibly fine, though with a possible improvement.

Now, in general the pattern where we have multiple threads setting a shared value on first use, to not lock on the value being obtained and set can be:

  1. Disastrous - other code will assume only one instance exists.
  2. Disastrous - the code that obtains the instance is not can only tolerate one (or perhaps a certain small number) concurrent operations.
  3. Disastrous - the means of storage is not thread-safe (e.g. have two threads adding to a dictionary and you can get all sorts of nasty errors).
  4. Sub-optimal - the overall performance is worse than if locking had ensured only one thread did the work of obtaining the value.
  5. Optimal - the cost of having multiple threads do redundant work is less than the cost of preventing it, especially since that can only happen during a relatively brief period.

However, considering here that MemoryCache may evict entries then:

  1. If it's disastrous to have more than one instance then MemoryCache is the wrong approach.
  2. If you must prevent simultaneous creation, you should do so at the point of creation.
  3. MemoryCache is thread-safe in terms of access to that object, so that is not a concern here.

Both of these possibilities have to be thought about of course, though the only time having two instances of the same string existing can be a problem is if you're doing very particular optimisations that don't apply here*.

So, we're left with the possibilities:

  1. It is cheaper to avoid the cost of duplicate calls to SomeHeavyAndExpensiveCalculation().
  2. It is cheaper not to avoid the cost of duplicate calls to SomeHeavyAndExpensiveCalculation().

And working that out can be difficult (indeed, the sort of thing where it's worth profiling rather than assuming you can work it out). It's worth considering here though that most obvious ways of locking on insert will prevent all additions to the cache, including those that are unrelated.

This means that if we had 50 threads trying to set 50 different values, then we'll have to make all 50 threads wait on each other, even though they weren't even going to do the same calculation.

As such, you're probably better off with the code you have, than with code that avoids the race-condition, and if the race-condition is a problem, you quite likely either need to handle that somewhere else, or need a different caching strategy than one that expels old entries†.

The one thing I would change is I'd replace the call to Set() with one to AddOrGetExisting(). From the above it should be clear that it probably isn't necessary, but it would allow the newly obtained item to be collected, reducing overall memory use and allowing a higher ratio of low generation to high generation collections.

So yeah, you could use double-locking to prevent concurrency, but either the concurrency isn't actually a problem, or your storing the values in the wrong way, or double-locking on the store would not be the best way to solve it.

*If you know only one each of a set of strings exists, you can optimise equality comparisons, which is about the only time having two copies of a string can be incorrect rather than just sub-optimal, but you'd want to be doing very different types of caching for that to make sense. E.g. the sort XmlReader does internally.

†Quite likely either one that stores indefinitely, or one that makes use of weak references so it will only expel entries if there are no existing uses.

Jon Hanna
  • 110,372
  • 10
  • 146
  • 251
2

Somewhat dated question, but maybe still useful: you may take a look at FusionCache ⚡, which I recently released.

The feature you are looking for is described here, and you can use it like this:

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    return fusionCache.GetOrSet(
        CacheKey,
        _ => SomeHeavyAndExpensiveCalculation(),
        TimeSpan.FromMinutes(20)
    );
}

You may also find some of the other features interesting like fail-safe, advanced timeouts with background factory completion and support for an optional, distributed 2nd level cache.

If you will give it a chance please let me know what you think.

/shameless-plug

Jody Donetti
  • 463
  • 6
  • 19
1

It is difficult to choose which one is better; lock or ReaderWriterLockSlim. You need real world statistics of read and write numbers and ratios etc.

But if you believe using "lock" is the correct way. Then here is a different solution for different needs. I also include the Allan Xu's solution in the code. Because both can be needed for different needs.

Here are the requirements, driving me to this solution:

  1. You don't want to or cannot supply the 'GetData' function for some reason. Perhaps the 'GetData' function is located in some other class with a heavy constructor and you do not want to even create an instance till ensuring it is unescapable.
  2. You need to access the same cached data from different locations/tiers of the application. And those different locations don't have access to same locker object.
  3. You don't have a constant cache key. For example; need of caching some data with the sessionId cache key.

Code:

using System;
using System.Runtime.Caching;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            //Allan Xu's usage
            string xyzData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);

            //My usage
            string sessionId = System.Web.HttpContext.Current.Session["CurrentUser.SessionId"].ToString();
            string yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
            if (string.IsNullOrWhiteSpace(yvz))
            {
                object locker = MemoryCacheHelper.GetLocker(sessionId);
                lock (locker)
                {
                    yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
                    if (string.IsNullOrWhiteSpace(yvz))
                    {
                        DatabaseRepositoryWithHeavyConstructorOverHead dbRepo = new DatabaseRepositoryWithHeavyConstructorOverHead();
                        yvz = dbRepo.GetDataExpensiveDataForSession(sessionId);
                        MemoryCacheHelper.AddDataToCache(sessionId, yvz, 5);
                    }
                }
            }
        }


        private static string SomeHeavyAndExpensiveXYZCalculation() { return "Expensive"; }
        private static string SomeHeavyAndExpensiveABCCalculation() { return "Expensive"; }

        public static class MemoryCacheHelper
        {
            //Allan Xu's solution
            public static T GetCachedDataOrAdd<T>(string cacheKey, object cacheLock, int minutesToExpire, Func<T> GetData) where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                    return cachedData;

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                        return cachedData;

                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, DateTime.Now.AddMinutes(minutesToExpire));
                    return cachedData;
                }
            }

            #region "My Solution"

            readonly static ConcurrentDictionary<string, object> Lockers = new ConcurrentDictionary<string, object>();
            public static object GetLocker(string cacheKey)
            {
                CleanupLockers();

                return Lockers.GetOrAdd(cacheKey, item => (cacheKey, new object()));
            }

            public static T GetCachedData<T>(string cacheKey) where T : class
            {
                CleanupLockers();

                T cachedData = MemoryCache.Default.Get(cacheKey) as T;
                return cachedData;
            }

            public static void AddDataToCache(string cacheKey, object value, int cacheTimePolicyMinutes)
            {
                CleanupLockers();

                MemoryCache.Default.Add(cacheKey, value, DateTimeOffset.Now.AddMinutes(cacheTimePolicyMinutes));
            }

            static DateTimeOffset lastCleanUpTime = DateTimeOffset.MinValue;
            static void CleanupLockers()
            {
                if (DateTimeOffset.Now.Subtract(lastCleanUpTime).TotalMinutes > 1)
                {
                    lock (Lockers)//maybe a better locker is needed?
                    {
                        try//bypass exceptions
                        {
                            List<string> lockersToRemove = new List<string>();
                            foreach (var locker in Lockers)
                            {
                                if (!MemoryCache.Default.Contains(locker.Key))
                                    lockersToRemove.Add(locker.Key);
                            }

                            object dummy;
                            foreach (string lockerKey in lockersToRemove)
                                Lockers.TryRemove(lockerKey, out dummy);

                            lastCleanUpTime = DateTimeOffset.Now;
                        }
                        catch (Exception)
                        { }
                    }
                }

            }
            #endregion
        }
    }

    class DatabaseRepositoryWithHeavyConstructorOverHead
    {
        internal string GetDataExpensiveDataForSession(string sessionId)
        {
            return "Expensive data from database";
        }
    }

}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
yvzman
  • 150
  • 1
  • 4
1

To avoid the global lock, you can use SingletonCache to implement one lock per key, without exploding memory usage (the lock objects are removed when no longer referenced, and acquire/release is thread safe guaranteeing that only 1 instance is ever in use via compare and swap).

Using it looks like this:

SingletonCache<string, object> keyLocks = new SingletonCache<string, object>();

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        return MemoryCache.Default[CacheKey] as string;
    }

    // double checked lock
    using (var lifetime = keyLocks.Acquire(url))
    {
        lock (lifetime.Value)
        {
           if (MemoryCache.Default.Contains(CacheKey))
           {
              return MemoryCache.Default[CacheKey] as string;
           }

           cacheItemPolicy cip = new CacheItemPolicy()
           {
              AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
           };
           expensiveString = SomeHeavyAndExpensiveCalculation();
           MemoryCache.Default.Set(CacheKey, expensiveString, cip);
           return expensiveString;
        }
    }      
}

Code is here on GitHub: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

There is also an LRU implementation that is lighter weight than MemoryCache, and has several advantages - faster concurrent reads and writes, bounded size, no background thread, internal perf counters etc. (disclaimer, I wrote it).

Alex Peck
  • 4,603
  • 1
  • 33
  • 37
0

Console example of MemoryCache, "How to save/get simple class objects"

Output after launching and pressing Any key except Esc :

Saving to cache!
Getting from cache!
Some1
Some2

    class Some
    {
        public String text { get; set; }

        public Some(String text)
        {
            this.text = text;
        }

        public override string ToString()
        {
            return text;
        }
    }

    public static MemoryCache cache = new MemoryCache("cache");

    public static string cache_name = "mycache";

    static void Main(string[] args)
    {

        Some some1 = new Some("some1");
        Some some2 = new Some("some2");

        List<Some> list = new List<Some>();
        list.Add(some1);
        list.Add(some2);

        do {

            if (cache.Contains(cache_name))
            {
                Console.WriteLine("Getting from cache!");
                List<Some> list_c = cache.Get(cache_name) as List<Some>;
                foreach (Some s in list_c) Console.WriteLine(s);
            }
            else
            {
                Console.WriteLine("Saving to cache!");
                cache.Set(cache_name, list, DateTime.Now.AddMinutes(10));                   
            }

        } while (Console.ReadKey(true).Key != ConsoleKey.Escape);

    }
fr0ga
  • 333
  • 4
  • 9
0
public interface ILazyCacheProvider : IAppCache
{
    /// <summary>
    /// Get data loaded - after allways throw cached result (even when data is older then needed) but very fast!
    /// </summary>
    /// <param name="key"></param>
    /// <param name="getData"></param>
    /// <param name="slidingExpiration"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    T GetOrAddPermanent<T>(string key, Func<T> getData, TimeSpan slidingExpiration);
}

/// <summary>
/// Initialize LazyCache in runtime
/// </summary>
public class LazzyCacheProvider: CachingService, ILazyCacheProvider
{
    private readonly Logger _logger = LogManager.GetLogger("MemCashe");
    private readonly Hashtable _hash = new Hashtable();
    private readonly List<string>  _reloader = new List<string>();
    private readonly ConcurrentDictionary<string, DateTime> _lastLoad = new ConcurrentDictionary<string, DateTime>();  


    T ILazyCacheProvider.GetOrAddPermanent<T>(string dataKey, Func<T> getData, TimeSpan slidingExpiration)
    {
        var currentPrincipal = Thread.CurrentPrincipal;
        if (!ObjectCache.Contains(dataKey) && !_hash.Contains(dataKey))
        {
            _hash[dataKey] = null;
            _logger.Debug($"{dataKey} - first start");
            _lastLoad[dataKey] = DateTime.Now;
            _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
            _lastLoad[dataKey] = DateTime.Now;
           _logger.Debug($"{dataKey} - first");
        }
        else
        {
            if ((!ObjectCache.Contains(dataKey) || _lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) < DateTime.Now) && _hash[dataKey] != null)
                Task.Run(() =>
                {
                    if (_reloader.Contains(dataKey)) return;
                    lock (_reloader)
                    {
                        if (ObjectCache.Contains(dataKey))
                        {
                            if(_lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) > DateTime.Now)
                                return;
                            _lastLoad[dataKey] = DateTime.Now;
                            Remove(dataKey);
                        }
                        _reloader.Add(dataKey);
                        Thread.CurrentPrincipal = currentPrincipal;
                        _logger.Debug($"{dataKey} - reload start");
                        _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
                        _logger.Debug($"{dataKey} - reload");
                        _reloader.Remove(dataKey);
                    }
                });
        }
        if (_hash[dataKey] != null) return (T) (_hash[dataKey]);

        _logger.Debug($"{dataKey} - dummy start");
        var data = GetOrAdd(dataKey, getData, slidingExpiration);
        _logger.Debug($"{dataKey} - dummy");
        return (T)((object)data).CloneObject();
    }
}
art24war
  • 57
  • 1
  • 2
0

Its a bit late, however... Full implementation:

    [HttpGet]
    public async Task<HttpResponseMessage> GetPageFromUriOrBody(RequestQuery requestQuery)
    {
        log(nameof(GetPageFromUriOrBody), nameof(requestQuery));
        var responseResult = await _requestQueryCache.GetOrCreate(
            nameof(GetPageFromUriOrBody)
            , requestQuery
            , (x) => getPageContent(x).Result);
        return Request.CreateResponse(System.Net.HttpStatusCode.Accepted, responseResult);
    }
    static MemoryCacheWithPolicy<RequestQuery, string> _requestQueryCache = new MemoryCacheWithPolicy<RequestQuery, string>();

Here is getPageContent signature:

async Task<string> getPageContent(RequestQuery requestQuery);

And here is the MemoryCacheWithPolicy implementation:

public class MemoryCacheWithPolicy<TParameter, TResult>
{
    static ILogger _nlogger = new AppLogger().Logger;
    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions() 
    {
        //Size limit amount: this is actually a memory size limit value!
        SizeLimit = 1024 
    });

    /// <summary>
    /// Gets or creates a new memory cache record for a main data
    /// along with parameter data that is assocciated with main main.
    /// </summary>
    /// <param name="key">Main data cache memory key.</param>
    /// <param name="param">Parameter model that assocciated to main model (request result).</param>
    /// <param name="createCacheData">A delegate to create a new main data to cache.</param>
    /// <returns></returns>
    public async Task<TResult> GetOrCreate(object key, TParameter param, Func<TParameter, TResult> createCacheData)
    {
        // this key is used for param cache memory.
        var paramKey = key + nameof(param);

        if (!_cache.TryGetValue(key, out TResult cacheEntry))
        {
            // key is not in the cache, create data through the delegate.
            cacheEntry = createCacheData(param);
            createMemoryCache(key, cacheEntry, paramKey, param);

            _nlogger.Warn(" cache is created.");
        }
        else
        {
            // data is chached so far..., check if param model is same (or changed)?
            if(!_cache.TryGetValue(paramKey, out TParameter cacheParam))
            {
                //exception: this case should not happened!
            }

            if (!cacheParam.Equals(param))
            {
                // request param is changed, create data through the delegate.
                cacheEntry = createCacheData(param);
                createMemoryCache(key, cacheEntry, paramKey, param);
                _nlogger.Warn(" cache is re-created (param model has been changed).");
            }
            else
            {
                _nlogger.Trace(" cache is used.");
            }

        }
        return await Task.FromResult<TResult>(cacheEntry);
    }
    MemoryCacheEntryOptions createMemoryCacheEntryOptions(TimeSpan slidingOffset, TimeSpan relativeOffset)
    {
        // Cache data within [slidingOffset] seconds, 
        // request new result after [relativeOffset] seconds.
        return new MemoryCacheEntryOptions()

            // Size amount: this is actually an entry count per 
            // key limit value! not an actual memory size value!
            .SetSize(1)

            // Priority on removing when reaching size limit (memory pressure)
            .SetPriority(CacheItemPriority.High)

            // Keep in cache for this amount of time, reset it if accessed.
            .SetSlidingExpiration(slidingOffset)

            // Remove from cache after this time, regardless of sliding expiration
            .SetAbsoluteExpiration(relativeOffset);
        //
    }
    void createMemoryCache(object key, TResult cacheEntry, object paramKey, TParameter param)
    {
        // Cache data within 2 seconds, 
        // request new result after 5 seconds.
        var cacheEntryOptions = createMemoryCacheEntryOptions(
            TimeSpan.FromSeconds(2)
            , TimeSpan.FromSeconds(5));

        // Save data in cache.
        _cache.Set(key, cacheEntry, cacheEntryOptions);

        // Save param in cache.
        _cache.Set(paramKey, param, cacheEntryOptions);
    }
    void checkCacheEntry<T>(object key, string name)
    {
        _cache.TryGetValue(key, out T value);
        _nlogger.Fatal("Key: {0}, Name: {1}, Value: {2}", key, name, value);
    }
}

nlogger is just nLog object to trace MemoryCacheWithPolicy behavior. I re-create the memory cache if request object (RequestQuery requestQuery) is changed through the delegate (Func<TParameter, TResult> createCacheData) or re-create when sliding or absolute time reached their limit. Note that everything is async too ;)

Sam Saarian
  • 992
  • 10
  • 13