⚠️ Warning: Don't cache lazy IEnumerable<T> in .NET

powerz

Aleksei Zagoskin

Posted on February 10, 2023

⚠️ Warning: Don't cache lazy IEnumerable<T> in .NET

(The same is true for IAsyncEnumerable<T>)

A few days ago I came across a code where a developer used .NET MemoryCache to cache data retrieved from the database. Sounds boring, but... every time the cache needed to return a result, a call to the database was made.

Let's take a look at a simplified example:

var serviceProvider = new ServiceCollection().AddMemoryCache().BuildServiceProvider();
var randomizer = Random.Shared;
var memoryCache = serviceProvider.GetRequiredService<IMemoryCache>();

// NOTE: we cache the result of GetData()
var data1 = memoryCache.GetOrCreate("key", _ => GetDataFromDb()).ToList();
var data2 = memoryCache.GetOrCreate("key", _ => GetDataFromDb()).ToList();

IEnumerable<int> GetDataFromDb()
{
    // This method will be called twice!
    yield return randomizer.Next();
}

Enter fullscreen mode Exit fullscreen mode

The problem here is that we cache not an array or collection as it may look at first glance, but a lazy IEnumerable, which is calculated not when we call the GetDateFromDb() method, but when we enumerate this lazy thing by calling ToList().

As a result, the method GetDataFromDb() gets called twice (this is what we wanted to avoid with the help of the cache), and also data1 and data will represent different lists with different values.

Solution

Cache the actual result enumerated result instead of lazy IEnumerable<T>:

var serviceProvider = new ServiceCollection().AddMemoryCache().BuildServiceProvider();
var randomizer = Random.Shared;
var memoryCache = serviceProvider.GetRequiredService<IMemoryCache>();

// NOTE: we cache GetData().ToList()
var data1 = memoryCache.GetOrCreate("key", _ => GetDataFromDb().ToList());
var data2 = memoryCache.GetOrCreate("key", _ => GetDataFromDb().ToList());

IEnumerable<int> GetDataFromDb()
{
    yield return randomizer.Next();
}

Enter fullscreen mode Exit fullscreen mode

Now, the behavior will be as expected: the GetDataFromDb() method gets called only once and data1 equals data2.

Thank you for reading and be careful with the cache.

Cheers!

💖 💪 🙅 🚩
powerz
Aleksei Zagoskin

Posted on February 10, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related