Skip to content

Commit 97e0c79

Browse files
AndyButlandkjac
andcommitted
Caching: Fixes regression of the caching of null representations for missing dictionary items (closes #20336 for 16) (#20349)
* Ports fix to regression of the caching of null representations for missing dictionary items. * Fixed error raised in code review. --------- Co-authored-by: Kenn Jacobsen <[email protected]>
1 parent 0c3e9fb commit 97e0c79

File tree

2 files changed

+176
-7
lines changed

2 files changed

+176
-7
lines changed

src/Umbraco.Core/Cache/AppCacheExtensions.cs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,27 +41,39 @@ public static void InsertCacheItem<T>(
4141
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey)
4242
{
4343
var result = provider.Get(cacheKey);
44-
if (IsRetrievedItemNull(result))
44+
if (result == null)
4545
{
4646
return default;
4747
}
4848

49+
// If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string).
50+
// Otherwise consider it a null value.
51+
if (RetrievedNullRepresentationInCache(result))
52+
{
53+
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
54+
}
55+
4956
return result.TryConvertTo<T>().Result;
5057
}
5158

5259
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey, Func<T> getCacheItem)
5360
{
5461
var result = provider.Get(cacheKey, () => getCacheItem());
55-
if (IsRetrievedItemNull(result))
62+
if (result == null)
5663
{
5764
return default;
5865
}
5966

67+
// If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string).
68+
// Otherwise consider it a null value.
69+
if (RetrievedNullRepresentationInCache(result))
70+
{
71+
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
72+
}
73+
6074
return result.TryConvertTo<T>().Result;
6175
}
6276

63-
private static bool IsRetrievedItemNull(object? result) => result is null or (object)Cms.Core.Constants.Cache.NullRepresentationInCache;
64-
6577
public static async Task<T?> GetCacheItemAsync<T>(
6678
this IAppPolicyCache provider,
6779
string cacheKey,
@@ -77,9 +89,25 @@ public static void InsertCacheItem<T>(
7789
provider.Insert(cacheKey, () => result, timeout, isSliding);
7890
}
7991

80-
return result == null ? default : result.TryConvertTo<T>().Result;
92+
if (result == null)
93+
{
94+
return default;
95+
}
96+
97+
// If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string).
98+
// Otherwise consider it a null value.
99+
if (RetrievedNullRepresentationInCache(result))
100+
{
101+
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
102+
}
103+
104+
return result.TryConvertTo<T>().Result;
81105
}
82106

107+
private static bool RetrievedNullRepresentationInCache(object result) => result == (object)Cms.Core.Constants.Cache.NullRepresentationInCache;
108+
109+
private static bool RequestedNullRepresentationInCache<T>() => typeof(T) == typeof(string);
110+
83111
public static async Task InsertCacheItemAsync<T>(
84112
this IAppPolicyCache provider,
85113
string cacheKey,

tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
// Copyright (c) Umbraco.
22
// See LICENSE for more details.
33

4-
using System.Collections.Generic;
5-
using System.Linq;
4+
using Microsoft.Extensions.Logging;
5+
using Moq;
66
using NUnit.Framework;
77
using Umbraco.Cms.Core;
8+
using Umbraco.Cms.Core.Cache;
89
using Umbraco.Cms.Core.Models;
910
using Umbraco.Cms.Core.Persistence.Repositories;
1011
using Umbraco.Cms.Core.Services;
12+
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
13+
using Umbraco.Cms.Infrastructure.Scoping;
1114
using Umbraco.Cms.Tests.Common.Testing;
1215
using Umbraco.Cms.Tests.Integration.Testing;
1316

@@ -22,6 +25,16 @@ internal sealed class DictionaryRepositoryTest : UmbracoIntegrationTest
2225

2326
private IDictionaryRepository CreateRepository() => GetRequiredService<IDictionaryRepository>();
2427

28+
private IDictionaryRepository CreateRepositoryWithCache(AppCaches cache) =>
29+
30+
// Create a repository with a real runtime cache.
31+
new DictionaryRepository(
32+
GetRequiredService<IScopeAccessor>(),
33+
cache,
34+
GetRequiredService<ILogger<DictionaryRepository>>(),
35+
GetRequiredService<ILoggerFactory>(),
36+
GetRequiredService<ILanguageRepository>());
37+
2538
[Test]
2639
public async Task Can_Perform_Get_By_Key_On_DictionaryRepository()
2740
{
@@ -396,6 +409,134 @@ public void Can_Perform_GetDictionaryItemKeyMap_On_DictionaryRepository()
396409
}
397410
}
398411

412+
[Test]
413+
public void Can_Perform_Cached_Request_For_Existing_Value_By_Key_On_DictionaryRepository_With_Cache()
414+
{
415+
var cache = AppCaches.Create(Mock.Of<IRequestCache>());
416+
var repository = CreateRepositoryWithCache(cache);
417+
418+
using (ScopeProvider.CreateScope())
419+
{
420+
var dictionaryItem = repository.Get("Read More");
421+
422+
Assert.AreEqual("Read More", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value);
423+
}
424+
425+
// Modify the value directly in the database. This won't be reflected in the repository cache and hence if the cache
426+
// is working as expected we should get the same value as above.
427+
using (var scope = ScopeProvider.CreateScope())
428+
{
429+
scope.Database.Execute("UPDATE cmsLanguageText SET value = 'Read More (updated)' WHERE value = 'Read More' and LanguageId = 1");
430+
scope.Complete();
431+
}
432+
433+
using (ScopeProvider.CreateScope())
434+
{
435+
var dictionaryItem = repository.Get("Read More");
436+
437+
Assert.AreEqual("Read More", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value);
438+
}
439+
440+
cache.IsolatedCaches.ClearCache<IDictionaryItem>();
441+
using (ScopeProvider.CreateScope())
442+
{
443+
var dictionaryItem = repository.Get("Read More");
444+
445+
Assert.AreEqual("Read More (updated)", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value);
446+
}
447+
}
448+
449+
[Test]
450+
public void Can_Perform_Cached_Request_For_NonExisting_Value_By_Key_On_DictionaryRepository_With_Cache()
451+
{
452+
var cache = AppCaches.Create(Mock.Of<IRequestCache>());
453+
var repository = CreateRepositoryWithCache(cache);
454+
455+
using (ScopeProvider.CreateScope())
456+
{
457+
var dictionaryItem = repository.Get("Read More Updated");
458+
459+
Assert.IsNull(dictionaryItem);
460+
}
461+
462+
// Modify the value directly in the database such that it now exists. This won't be reflected in the repository cache and hence if the cache
463+
// is working as expected we should get the same null value as above.
464+
using (var scope = ScopeProvider.CreateScope())
465+
{
466+
scope.Database.Execute("UPDATE cmsDictionary SET [key] = 'Read More Updated' WHERE [key] = 'Read More'");
467+
scope.Complete();
468+
}
469+
470+
using (ScopeProvider.CreateScope())
471+
{
472+
var dictionaryItem = repository.Get("Read More Updated");
473+
474+
Assert.IsNull(dictionaryItem);
475+
}
476+
477+
cache.IsolatedCaches.ClearCache<IDictionaryItem>();
478+
using (ScopeProvider.CreateScope())
479+
{
480+
var dictionaryItem = repository.Get("Read More Updated");
481+
482+
Assert.IsNotNull(dictionaryItem);
483+
}
484+
}
485+
486+
[Test]
487+
public void Cannot_Perform_Cached_Request_For_Existing_Value_By_Key_On_DictionaryRepository_Without_Cache()
488+
{
489+
var repository = CreateRepository();
490+
491+
using (ScopeProvider.CreateScope())
492+
{
493+
var dictionaryItem = repository.Get("Read More");
494+
495+
Assert.AreEqual("Read More", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value);
496+
}
497+
498+
// Modify the value directly in the database. As we don't have caching enabled on the repository we should get the new value.
499+
using (var scope = ScopeProvider.CreateScope())
500+
{
501+
scope.Database.Execute("UPDATE cmsLanguageText SET value = 'Read More (updated)' WHERE value = 'Read More' and LanguageId = 1");
502+
scope.Complete();
503+
}
504+
505+
using (ScopeProvider.CreateScope())
506+
{
507+
var dictionaryItem = repository.Get("Read More");
508+
509+
Assert.AreEqual("Read More (updated)", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value);
510+
}
511+
}
512+
513+
[Test]
514+
public void Cannot_Perform_Cached_Request_For_NonExisting_Value_By_Key_On_DictionaryRepository_Without_Cache()
515+
{
516+
var repository = CreateRepository();
517+
518+
using (ScopeProvider.CreateScope())
519+
{
520+
var dictionaryItem = repository.Get("Read More Updated");
521+
522+
Assert.IsNull(dictionaryItem);
523+
}
524+
525+
// Modify the value directly in the database such that it now exists. As we don't have caching enabled on the repository we should get the new value.
526+
using (var scope = ScopeProvider.CreateScope())
527+
{
528+
scope.Database.Execute("UPDATE cmsDictionary SET [key] = 'Read More Updated' WHERE [key] = 'Read More'");
529+
scope.Complete();
530+
}
531+
532+
using (ScopeProvider.CreateScope())
533+
{
534+
var dictionaryItem = repository.Get("Read More Updated");
535+
536+
Assert.IsNotNull(dictionaryItem);
537+
}
538+
}
539+
399540
public async Task CreateTestData()
400541
{
401542
var languageService = GetRequiredService<ILanguageService>();

0 commit comments

Comments
 (0)