Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions src/Umbraco.Core/Cache/AppCacheExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,39 @@
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey)
{
var result = provider.Get(cacheKey);
if (IsRetrievedItemNull(result))
if (result == null)
{
return default;
}

// 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).
// Otherwise consider it a null value.
if (RetrievedNullRepresentationInCache(result))
{
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
}

Check warning on line 55 in src/Umbraco.Core/Cache/AppCacheExtensions.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Code Duplication

The module contains 2 functions with similar structure: GetCacheItem,GetCacheItem. Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.
return result.TryConvertTo<T>().Result;
}

public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey, Func<T> getCacheItem)
{
var result = provider.Get(cacheKey, () => getCacheItem());
if (IsRetrievedItemNull(result))
if (result == null)
{
return default;
}

// 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).
// Otherwise consider it a null value.
if (RetrievedNullRepresentationInCache(result))
{
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
}

return result.TryConvertTo<T>().Result;
}

private static bool IsRetrievedItemNull(object? result) => result is null or (object)Cms.Core.Constants.Cache.NullRepresentationInCache;

public static async Task<T?> GetCacheItemAsync<T>(
this IAppPolicyCache provider,
string cacheKey,
Expand All @@ -77,9 +89,25 @@
provider.Insert(cacheKey, () => result, timeout, isSliding);
}

return result == null ? default : result.TryConvertTo<T>().Result;
if (result == null)
{
return default;
}

// 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).
// Otherwise consider it a null value.
if (RetrievedNullRepresentationInCache(result))
{
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
}

return result.TryConvertTo<T>().Result;
}

private static bool RetrievedNullRepresentationInCache(object result) => result == (object)Cms.Core.Constants.Cache.NullRepresentationInCache;

private static bool RequestedNullRepresentationInCache<T>() => typeof(T) == typeof(string);

public static async Task InsertCacheItemAsync<T>(
this IAppPolicyCache provider,
string cacheKey,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// Copyright (c) Umbraco.

Check warning on line 1 in tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Code Duplication

introduced similar code in: Can_Perform_Cached_Request_For_Existing_Value_By_Key_On_DictionaryRepository_With_Cache,Can_Perform_Cached_Request_For_NonExisting_Value_By_Key_On_DictionaryRepository_With_Cache,Cannot_Perform_Cached_Request_For_Existing_Value_By_Key_On_DictionaryRepository_Without_Cache,Cannot_Perform_Cached_Request_For_NonExisting_Value_By_Key_On_DictionaryRepository_Without_Cache. Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.
// 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;

Expand All @@ -22,6 +25,16 @@

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()
{
Expand Down Expand Up @@ -396,6 +409,134 @@
}
}

[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>();
Expand Down