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 <kja@umbraco.dk>
This commit is contained in:
Andy Butland
2025-10-02 20:10:16 +02:00
committed by GitHub
parent e6d1d7a6b9
commit 61277b3812
2 changed files with 176 additions and 7 deletions

View File

@@ -1,13 +1,16 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
@@ -22,6 +25,16 @@ internal sealed class DictionaryRepositoryTest : UmbracoIntegrationTest
private IDictionaryRepository CreateRepository() => GetRequiredService<IDictionaryRepository>();
private IDictionaryRepository CreateRepositoryWithCache(AppCaches cache) =>
// Create a repository with a real runtime cache.
new DictionaryRepository(
GetRequiredService<IScopeAccessor>(),
cache,
GetRequiredService<ILogger<DictionaryRepository>>(),
GetRequiredService<ILoggerFactory>(),
GetRequiredService<ILanguageRepository>());
[Test]
public async Task Can_Perform_Get_By_Key_On_DictionaryRepository()
{
@@ -396,6 +409,134 @@ internal sealed class DictionaryRepositoryTest : UmbracoIntegrationTest
}
}
[Test]
public void Can_Perform_Cached_Request_For_Existing_Value_By_Key_On_DictionaryRepository_With_Cache()
{
var cache = AppCaches.Create(Mock.Of<IRequestCache>());
var repository = CreateRepositoryWithCache(cache);
using (ScopeProvider.CreateScope())
{
var dictionaryItem = repository.Get("Read More");
Assert.AreEqual("Read More", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value);
}
// Modify the value directly in the database. This won't be reflected in the repository cache and hence if the cache
// is working as expected we should get the same value as above.
using (var scope = ScopeProvider.CreateScope())
{
scope.Database.Execute("UPDATE cmsLanguageText SET value = 'Read More (updated)' WHERE value = 'Read More' and LanguageId = 1");
scope.Complete();
}
using (ScopeProvider.CreateScope())
{
var dictionaryItem = repository.Get("Read More");
Assert.AreEqual("Read More", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value);
}
cache.IsolatedCaches.ClearCache<IDictionaryItem>();
using (ScopeProvider.CreateScope())
{
var dictionaryItem = repository.Get("Read More");
Assert.AreEqual("Read More (updated)", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value);
}
}
[Test]
public void Can_Perform_Cached_Request_For_NonExisting_Value_By_Key_On_DictionaryRepository_With_Cache()
{
var cache = AppCaches.Create(Mock.Of<IRequestCache>());
var repository = CreateRepositoryWithCache(cache);
using (ScopeProvider.CreateScope())
{
var dictionaryItem = repository.Get("Read More Updated");
Assert.IsNull(dictionaryItem);
}
// 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
// is working as expected we should get the same null value as above.
using (var scope = ScopeProvider.CreateScope())
{
scope.Database.Execute("UPDATE cmsDictionary SET [key] = 'Read More Updated' WHERE [key] = 'Read More'");
scope.Complete();
}
using (ScopeProvider.CreateScope())
{
var dictionaryItem = repository.Get("Read More Updated");
Assert.IsNull(dictionaryItem);
}
cache.IsolatedCaches.ClearCache<IDictionaryItem>();
using (ScopeProvider.CreateScope())
{
var dictionaryItem = repository.Get("Read More Updated");
Assert.IsNotNull(dictionaryItem);
}
}
[Test]
public void Cannot_Perform_Cached_Request_For_Existing_Value_By_Key_On_DictionaryRepository_Without_Cache()
{
var repository = CreateRepository();
using (ScopeProvider.CreateScope())
{
var dictionaryItem = repository.Get("Read More");
Assert.AreEqual("Read More", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value);
}
// Modify the value directly in the database. As we don't have caching enabled on the repository we should get the new value.
using (var scope = ScopeProvider.CreateScope())
{
scope.Database.Execute("UPDATE cmsLanguageText SET value = 'Read More (updated)' WHERE value = 'Read More' and LanguageId = 1");
scope.Complete();
}
using (ScopeProvider.CreateScope())
{
var dictionaryItem = repository.Get("Read More");
Assert.AreEqual("Read More (updated)", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value);
}
}
[Test]
public void Cannot_Perform_Cached_Request_For_NonExisting_Value_By_Key_On_DictionaryRepository_Without_Cache()
{
var repository = CreateRepository();
using (ScopeProvider.CreateScope())
{
var dictionaryItem = repository.Get("Read More Updated");
Assert.IsNull(dictionaryItem);
}
// 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.
using (var scope = ScopeProvider.CreateScope())
{
scope.Database.Execute("UPDATE cmsDictionary SET [key] = 'Read More Updated' WHERE [key] = 'Read More'");
scope.Complete();
}
using (ScopeProvider.CreateScope())
{
var dictionaryItem = repository.Get("Read More Updated");
Assert.IsNotNull(dictionaryItem);
}
}
public async Task CreateTestData()
{
var languageService = GetRequiredService<ILanguageService>();