diff --git a/src/Umbraco.Core/Cache/DictionaryAppCache.cs b/src/Umbraco.Core/Cache/DictionaryAppCache.cs index 5bf5848309..fa0ec1b0e0 100644 --- a/src/Umbraco.Core/Cache/DictionaryAppCache.cs +++ b/src/Umbraco.Core/Cache/DictionaryAppCache.cs @@ -24,7 +24,19 @@ public class DictionaryAppCache : IRequestCache public virtual object? Get(string key) => _items.TryGetValue(key, out var value) ? value : null; /// - public virtual object? Get(string key, Func factory) => _items.GetOrAdd(key, _ => factory()); + public virtual object? Get(string key, Func factory) + { + var value = _items.GetOrAdd(key, _ => factory()); + + if (value is not null) + { + return value; + } + + // do not cache null values + _items.TryRemove(key, out _); + return null; + } public bool Set(string key, object? value) => _items.TryAdd(key, value); diff --git a/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs b/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs index 6476c76f96..e99cdad899 100644 --- a/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs +++ b/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs @@ -31,6 +31,14 @@ public class FastDictionaryAppCache : IAppCache Lazy? result = _items.GetOrAdd(cacheKey, k => SafeLazy.GetSafeLazy(getCacheItem)); var value = result.Value; // will not throw (safe lazy) + + if (value is null) + { + // do not cache null values + _items.TryRemove(cacheKey, out _); + return null; + } + if (!(value is SafeLazy.ExceptionHolder eh)) { return value; diff --git a/src/Umbraco.Core/Cache/IAppCache.cs b/src/Umbraco.Core/Cache/IAppCache.cs index 187ff6fc11..99207e6c10 100644 --- a/src/Umbraco.Core/Cache/IAppCache.cs +++ b/src/Umbraco.Core/Cache/IAppCache.cs @@ -18,6 +18,7 @@ public interface IAppCache /// The key of the item. /// A factory function that can create the item. /// The item. + /// Null values returned from the factory function are never cached. object? Get(string key, Func factory); /// diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/AppCacheTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/AppCacheTests.cs index f818fa49e8..a207a257fe 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/AppCacheTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/AppCacheTests.cs @@ -81,6 +81,25 @@ public abstract class AppCacheTests Assert.Greater(counter, 1); } + [Test] + public void Does_Not_Cache_Null_Values() + { + var counter = 0; + + object? Factory() + { + counter++; + return counter == 3 ? "Not a null value" : null; + } + + object? Get() => AppCache.Get("Blah", Factory); + + Assert.IsNull(Get()); + Assert.IsNull(Get()); + Assert.AreEqual("Not a null value", Get()); + Assert.AreEqual(3, counter); + } + [Test] public void Ensures_Delegate_Result_Is_Cached_Once() { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/FastDictionaryAppCacheTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/FastDictionaryAppCacheTests.cs new file mode 100644 index 0000000000..efb6f0fc9e --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/FastDictionaryAppCacheTests.cs @@ -0,0 +1,20 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Cache; + +[TestFixture] +public class FastDictionaryAppCacheTests : AppCacheTests +{ + public override void Setup() + { + base.Setup(); + _appCache = new FastDictionaryAppCache(); + } + + private FastDictionaryAppCache _appCache; + + internal override IAppCache AppCache => _appCache; + + protected override int GetTotalItemCount => _appCache.Count; +}