Skip to content

Commit 7d87ce3

Browse files
AndyButlandkjac
andauthored
Caching: Fixes regression of the caching of null representations for missing dictionary items (#20344)
* Fixed regression with handling of null representation in cache. * Added integration test to verify behaviour. * Use helper methods. * Amend tests so they verify that cached null values are also removed --------- Co-authored-by: kjac <[email protected]>
1 parent d4805fa commit 7d87ce3

File tree

2 files changed

+162
-5
lines changed

2 files changed

+162
-5
lines changed

src/Umbraco.Core/Cache/AppCacheExtensions.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,24 +43,41 @@ public static void InsertCacheItem<T>(
4343
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey)
4444
{
4545
var result = provider.Get(cacheKey);
46-
if (IsRetrievedItemNull(result))
46+
if (result == null)
4747
{
4848
return default;
4949
}
5050

51+
// 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).
52+
// Otherwise consider it a null value.
53+
if (RetrievedNullRepresentationInCache(result))
54+
{
55+
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
56+
}
57+
5158
return result.TryConvertTo<T>().Result;
5259
}
5360

5461
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey, Func<T> getCacheItem)
5562
{
5663
var result = provider.Get(cacheKey, () => getCacheItem());
57-
if (IsRetrievedItemNull(result))
64+
if (result == null)
5865
{
5966
return default;
6067
}
6168

69+
// 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).
70+
// Otherwise consider it a null value.
71+
if (RetrievedNullRepresentationInCache(result))
72+
{
73+
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
74+
}
75+
6276
return result.TryConvertTo<T>().Result;
6377
}
6478

65-
private static bool IsRetrievedItemNull(object? result) => result is null or (object)Cms.Core.Constants.Cache.NullRepresentationInCache;
79+
private static bool RetrievedNullRepresentationInCache(object result) => result == (object)Cms.Core.Constants.Cache.NullRepresentationInCache;
80+
81+
private static bool RequestedNullRepresentationInCache<T>() => typeof(T) == typeof(string);
82+
6683
}

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

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
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;
7+
using Umbraco.Cms.Core.Cache;
78
using Umbraco.Cms.Core.Models;
89
using Umbraco.Cms.Core.Persistence.Repositories;
910
using Umbraco.Cms.Core.Services;
11+
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
12+
using Umbraco.Cms.Infrastructure.Scoping;
1013
using Umbraco.Cms.Tests.Common.Testing;
1114
using Umbraco.Cms.Tests.Integration.Testing;
1215

@@ -21,6 +24,15 @@ public class DictionaryRepositoryTest : UmbracoIntegrationTest
2124

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

27+
private IDictionaryRepository CreateRepositoryWithCache(AppCaches cache) =>
28+
29+
// Create a repository with a real runtime cache.
30+
new DictionaryRepository(
31+
GetRequiredService<IScopeAccessor>(),
32+
cache,
33+
GetRequiredService<ILogger<DictionaryRepository>>(),
34+
GetRequiredService<ILoggerFactory>());
35+
2436
[Test]
2537
public void Can_Perform_Get_By_Key_On_DictionaryRepository()
2638
{
@@ -395,6 +407,134 @@ public void Can_Perform_GetDictionaryItemKeyMap_On_DictionaryRepository()
395407
}
396408
}
397409

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

0 commit comments

Comments
 (0)