Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
23 changes: 20 additions & 3 deletions src/Umbraco.Core/Cache/AppCacheExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,41 @@
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).
Comment thread
kjac marked this conversation as resolved.
// Otherwise consider it a null value.
if (RetrievedNullRepresentationInCache(result))
{
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
}

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

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (v13/dev)

❌ 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 (result == (object)Cms.Core.Constants.Cache.NullRepresentationInCache)
{
return typeof(T) == typeof(string) ? (T)result : default;
Comment thread
AndyButland marked this conversation as resolved.
Outdated
}

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

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

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

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// 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 (v13/dev)

❌ 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.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 @@ -21,6 +24,15 @@

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

private IDictionaryRepository CreateRepositoryWithCache() =>

// Create a repository with a real runtime cache.
new DictionaryRepository(
GetRequiredService<IScopeAccessor>(),
AppCaches.Create(Mock.Of<IRequestCache>()),
GetRequiredService<ILogger<DictionaryRepository>>(),
GetRequiredService<ILoggerFactory>());

[Test]
public void Can_Perform_Get_By_Key_On_DictionaryRepository()
{
Expand Down Expand Up @@ -395,6 +407,116 @@
}
}

[Test]
public void Can_Perform_Cached_Request_For_Existing_Value_By_Key_On_DictionaryRepository_With_Cache()
{
var repository = CreateRepositoryWithCache();

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);
}
}

[Test]
public void Can_Perform_Cached_Request_For_NonExisting_Value_By_Key_On_DictionaryRepository_With_Cache()
{
var repository = CreateRepositoryWithCache();

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'");
Comment thread
AndyButland marked this conversation as resolved.
scope.Complete();
}

using (ScopeProvider.CreateScope())
{
var dictionaryItem = repository.Get("Read More Updated");

Assert.IsNull(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'");
Comment thread
AndyButland marked this conversation as resolved.
scope.Complete();
}

using (ScopeProvider.CreateScope())
{
var dictionaryItem = repository.Get("Read More Updated");

Assert.IsNotNull(dictionaryItem);
}
}

public void CreateTestData()
{
var localizationService = GetRequiredService<ILocalizationService>();
Expand Down
Loading