diff --git a/src/Umbraco.Core/Cache/AppCacheExtensions.cs b/src/Umbraco.Core/Cache/AppCacheExtensions.cs index 55079ba018..480b677f24 100644 --- a/src/Umbraco.Core/Cache/AppCacheExtensions.cs +++ b/src/Umbraco.Core/Cache/AppCacheExtensions.cs @@ -41,7 +41,7 @@ public static class AppCacheExtensions public static T? GetCacheItem(this IAppCache provider, string cacheKey) { var result = provider.Get(cacheKey); - if (result == null) + if (IsRetrievedItemNull(result)) { return default; } @@ -52,7 +52,7 @@ public static class AppCacheExtensions public static T? GetCacheItem(this IAppCache provider, string cacheKey, Func getCacheItem) { var result = provider.Get(cacheKey, () => getCacheItem()); - if (result == null) + if (IsRetrievedItemNull(result)) { return default; } @@ -60,6 +60,8 @@ public static class AppCacheExtensions return result.TryConvertTo().Result; } + private static bool IsRetrievedItemNull(object? result) => result is null or (object)Cms.Core.Constants.Cache.NullRepresentationInCache; + public static async Task GetCacheItemAsync( this IAppPolicyCache provider, string cacheKey, diff --git a/src/Umbraco.Core/Constants-Cache.cs b/src/Umbraco.Core/Constants-Cache.cs index ccc347111e..874605c123 100644 --- a/src/Umbraco.Core/Constants-Cache.cs +++ b/src/Umbraco.Core/Constants-Cache.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core; +namespace Umbraco.Cms.Core; public static partial class Constants { public static class Cache @@ -9,5 +9,13 @@ public static partial class Constants public const string Media = "media"; } + + /// + /// Defines the string used to represent a null value in the cache. + /// + /// + /// Used in conjunction with the option to cache null values on the repository caches, so we + /// can distinguish a true null "not found" value and a cached null value. + public const string NullRepresentationInCache = "*NULL*"; } } diff --git a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs index 9494ed2eea..0ac9f89b79 100644 --- a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs @@ -24,8 +24,6 @@ public class DefaultRepositoryCachePolicy : RepositoryCachePolicyB private static readonly TEntity[] _emptyEntities = new TEntity[0]; // const private readonly RepositoryCachePolicyOptions _options; - private const string NullRepresentationInCache = "*NULL*"; - public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) : base(cache, scopeAccessor) => _options = options ?? throw new ArgumentNullException(nameof(options)); @@ -139,10 +137,8 @@ public class DefaultRepositoryCachePolicy : RepositoryCachePolicyB return fromCache; } - // Because TEntity can never be a string, we will never be in a position where the proxy value collides withs a real value. - // Therefore this point can only be reached if there is a proxy null value => becomes null when cast to TEntity above OR the item simply does not exist. // If we've cached a "null" value, return null. - if (_options.CacheNullValues && Cache.GetCacheItem(cacheKey) == NullRepresentationInCache) + if (_options.CacheNullValues && Cache.GetCacheItem(cacheKey) == Constants.Cache.NullRepresentationInCache) { return null; } @@ -273,7 +269,7 @@ public class DefaultRepositoryCachePolicy : RepositoryCachePolicyB // a value that does exist but isn't yet cached, or a value that has been explicitly cached with a null value. // Both would return null when we retrieve from the cache and we couldn't distinguish between the two. // So we cache a special value that represents null, and then we can check for that value when we retrieve from the cache. - Cache.Insert(cacheKey, () => NullRepresentationInCache, TimeSpan.FromMinutes(5), true); + Cache.Insert(cacheKey, () => Constants.Cache.NullRepresentationInCache, TimeSpan.FromMinutes(5), true); } protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities)