diff --git a/src/Umbraco.PublishedCache.HybridCache/Factories/IPublishedContentFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Factories/IPublishedContentFactory.cs
index c5bfe4fe9e..46cb65d5e4 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Factories/IPublishedContentFactory.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Factories/IPublishedContentFactory.cs
@@ -1,12 +1,25 @@
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Infrastructure.HybridCache.Factories;
+///
+/// Defines a factory to create and from a or .
+///
internal interface IPublishedContentFactory
{
+ ///
+ /// Converts a to an if document type.
+ ///
IPublishedContent? ToIPublishedContent(ContentCacheNode contentCacheNode, bool preview);
+
+ ///
+ /// Converts a to an of media type.
+ ///
IPublishedContent? ToIPublishedMedia(ContentCacheNode contentCacheNode);
+ ///
+ /// Converts a to an .
+ ///
IPublishedMember ToPublishedMember(IMember member);
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs
index 57863dc986..5f33705716 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs
@@ -1,29 +1,61 @@
-using Umbraco.Cms.Core.Models;
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
+using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.HybridCache.Factories;
+///
+/// Defines a factory to create and from a or .
+///
internal sealed class PublishedContentFactory : IPublishedContentFactory
{
private readonly IElementsCache _elementsCache;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
+ private readonly ILogger _logger;
+ private readonly AppCaches _appCaches;
-
+ ///
+ /// Initializes a new instance of the class.
+ ///
public PublishedContentFactory(
IElementsCache elementsCache,
IVariationContextAccessor variationContextAccessor,
- IPublishedContentTypeCache publishedContentTypeCache)
+ IPublishedContentTypeCache publishedContentTypeCache,
+ ILogger logger,
+ AppCaches appCaches)
{
_elementsCache = elementsCache;
_variationContextAccessor = variationContextAccessor;
_publishedContentTypeCache = publishedContentTypeCache;
+ _logger = logger;
+ _appCaches = appCaches;
}
+ ///
public IPublishedContent? ToIPublishedContent(ContentCacheNode contentCacheNode, bool preview)
{
- IPublishedContentType contentType = _publishedContentTypeCache.Get(PublishedItemType.Content, contentCacheNode.ContentTypeId);
+ var cacheKey = $"{nameof(PublishedContentFactory)}DocumentCache_{contentCacheNode.Id}_{preview}";
+ IPublishedContent? publishedContent = _appCaches.RequestCache.GetCacheItem(cacheKey);
+ if (publishedContent is not null)
+ {
+ _logger.LogDebug(
+ "Using cached IPublishedContent for document {ContentCacheNodeName} ({ContentCacheNodeId}).",
+ contentCacheNode.Data?.Name ?? "No Name",
+ contentCacheNode.Id);
+ return publishedContent;
+ }
+
+ _logger.LogDebug(
+ "Creating IPublishedContent for document {ContentCacheNodeName} ({ContentCacheNodeId}).",
+ contentCacheNode.Data?.Name ?? "No Name",
+ contentCacheNode.Id);
+
+ IPublishedContentType contentType =
+ _publishedContentTypeCache.Get(PublishedItemType.Content, contentCacheNode.ContentTypeId);
var contentNode = new ContentNode(
contentCacheNode.Id,
contentCacheNode.Key,
@@ -34,19 +66,42 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory
preview ? contentCacheNode.Data : null,
preview ? null : contentCacheNode.Data);
- IPublishedContent? model = GetModel(contentNode, preview);
+ publishedContent = GetModel(contentNode, preview);
if (preview)
{
- return model ?? GetPublishedContentAsDraft(model);
+ publishedContent ??= GetPublishedContentAsDraft(publishedContent);
}
- return model;
+ if (publishedContent is not null)
+ {
+ _appCaches.RequestCache.Set(cacheKey, publishedContent);
+ }
+
+ return publishedContent;
}
+ ///
public IPublishedContent? ToIPublishedMedia(ContentCacheNode contentCacheNode)
{
- IPublishedContentType contentType = _publishedContentTypeCache.Get(PublishedItemType.Media, contentCacheNode.ContentTypeId);
+ var cacheKey = $"{nameof(PublishedContentFactory)}MediaCache_{contentCacheNode.Id}";
+ IPublishedContent? publishedContent = _appCaches.RequestCache.GetCacheItem(cacheKey);
+ if (publishedContent is not null)
+ {
+ _logger.LogDebug(
+ "Using cached IPublishedContent for media {ContentCacheNodeName} ({ContentCacheNodeId}).",
+ contentCacheNode.Data?.Name ?? "No Name",
+ contentCacheNode.Id);
+ return publishedContent;
+ }
+
+ _logger.LogDebug(
+ "Creating IPublishedContent for media {ContentCacheNodeName} ({ContentCacheNodeId}).",
+ contentCacheNode.Data?.Name ?? "No Name",
+ contentCacheNode.Id);
+
+ IPublishedContentType contentType =
+ _publishedContentTypeCache.Get(PublishedItemType.Media, contentCacheNode.ContentTypeId);
var contentNode = new ContentNode(
contentCacheNode.Id,
contentCacheNode.Key,
@@ -57,14 +112,40 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory
null,
contentCacheNode.Data);
- return GetModel(contentNode, false);
+ publishedContent = GetModel(contentNode, false);
+
+ if (publishedContent is not null)
+ {
+ _appCaches.RequestCache.Set(cacheKey, publishedContent);
+ }
+
+ return publishedContent;
}
+ ///
public IPublishedMember ToPublishedMember(IMember member)
{
- IPublishedContentType contentType = _publishedContentTypeCache.Get(PublishedItemType.Member, member.ContentTypeId);
+ string cacheKey = $"{nameof(PublishedContentFactory)}MemberCache_{member.Id}";
+ IPublishedMember? publishedMember = _appCaches.RequestCache.GetCacheItem(cacheKey);
+ if (publishedMember is not null)
+ {
+ _logger.LogDebug(
+ "Using cached IPublishedMember for member {MemberName} ({MemberId}).",
+ member.Username,
+ member.Id);
- // Members are only "mapped" never cached, so these default values are a bit wierd, but they are not used.
+ return publishedMember;
+ }
+
+ _logger.LogDebug(
+ "Creating IPublishedMember for member {MemberName} ({MemberId}).",
+ member.Username,
+ member.Id);
+
+ IPublishedContentType contentType =
+ _publishedContentTypeCache.Get(PublishedItemType.Member, member.ContentTypeId);
+
+ // Members are only "mapped" never cached, so these default values are a bit weird, but they are not used.
var contentData = new ContentData(
member.Name,
null,
@@ -85,7 +166,11 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory
contentType,
null,
contentData);
- return new PublishedMember(member, contentNode, _elementsCache, _variationContextAccessor);
+ publishedMember = new PublishedMember(member, contentNode, _elementsCache, _variationContextAccessor);
+
+ _appCaches.RequestCache.Set(cacheKey, publishedMember);
+
+ return publishedMember;
}
private static Dictionary GetPropertyValues(IPublishedContentType contentType, IMember member)
@@ -134,7 +219,6 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory
_variationContextAccessor);
}
-
private static IPublishedContent? GetPublishedContentAsDraft(IPublishedContent? content) =>
content == null ? null :
// an object in the cache is either an IPublishedContentOrMedia,
@@ -149,7 +233,7 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory
content = wrapped.Unwrap();
}
- if (!(content is PublishedContent inner))
+ if (content is not PublishedContent inner)
{
throw new InvalidOperationException("Innermost content is not PublishedContent.");
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactoryTests.cs
new file mode 100644
index 0000000000..3d7be44d69
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactoryTests.cs
@@ -0,0 +1,147 @@
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Infrastructure.HybridCache;
+using Umbraco.Cms.Infrastructure.HybridCache.Factories;
+using Umbraco.Cms.Tests.Common.Builders;
+using Umbraco.Cms.Tests.Common.Builders.Extensions;
+using Umbraco.Cms.Tests.Common.Testing;
+using Umbraco.Cms.Tests.Integration.Testing;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
+
+[TestFixture]
+[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
+internal sealed class PublishedContentFactoryTests : UmbracoIntegrationTestWithContent
+{
+ private IPublishedContentFactory PublishedContentFactory => GetRequiredService();
+
+ private IPublishedValueFallback PublishedValueFallback => GetRequiredService();
+
+ private IMediaService MediaService => GetRequiredService();
+
+ private IMediaTypeService MediaTypeService => GetRequiredService();
+
+ private IMemberService MemberService => GetRequiredService();
+
+ private IMemberTypeService MemberTypeService => GetRequiredService();
+
+ protected override void CustomTestSetup(IUmbracoBuilder builder)
+ {
+ var requestCache = new DictionaryAppCache();
+ var appCaches = new AppCaches(
+ NoAppCache.Instance,
+ requestCache,
+ new IsolatedCaches(type => NoAppCache.Instance));
+ builder.Services.AddUnique(appCaches);
+ }
+
+ [Test]
+ public void Can_Create_Published_Content_For_Document()
+ {
+ var contentCacheNode = new ContentCacheNode
+ {
+ Id = Textpage.Id,
+ Key = Textpage.Key,
+ ContentTypeId = Textpage.ContentType.Id,
+ CreateDate = Textpage.CreateDate,
+ CreatorId = Textpage.CreatorId,
+ SortOrder = Textpage.SortOrder,
+ Data = new ContentData(
+ Textpage.Name,
+ "text-page",
+ Textpage.VersionId,
+ Textpage.UpdateDate,
+ Textpage.WriterId,
+ Textpage.TemplateId,
+ true,
+ new Dictionary
+ {
+ {
+ "title", new[]
+ {
+ new PropertyData
+ {
+ Value = "Test title",
+ Culture = string.Empty,
+ Segment = string.Empty,
+ },
+ }
+ },
+ },
+ null),
+ };
+ var result = PublishedContentFactory.ToIPublishedContent(contentCacheNode, false);
+ Assert.IsNotNull(result);
+ Assert.AreEqual(Textpage.Id, result.Id);
+ Assert.AreEqual(Textpage.Name, result.Name);
+ Assert.AreEqual("Test title", result.Properties.Single(x => x.Alias == "title").Value(PublishedValueFallback));
+
+ // Verify that requesting the same content again returns the same instance (from request cache).
+ var result2 = PublishedContentFactory.ToIPublishedContent(contentCacheNode, false);
+ Assert.AreSame(result, result2);
+ }
+
+ [Test]
+ public async Task Can_Create_Published_Content_For_Media()
+ {
+ var mediaType = new MediaTypeBuilder().Build();
+ mediaType.AllowedAsRoot = true;
+ await MediaTypeService.CreateAsync(mediaType, Constants.Security.SuperUserKey);
+
+ var media = new MediaBuilder()
+ .WithMediaType(mediaType)
+ .WithName("Media 1")
+ .Build();
+ MediaService.Save(media);
+
+ var contentCacheNode = new ContentCacheNode
+ {
+ Id = media.Id,
+ Key = media.Key,
+ ContentTypeId = media.ContentType.Id,
+ Data = new ContentData(
+ media.Name,
+ null,
+ 0,
+ media.UpdateDate,
+ media.WriterId,
+ null,
+ false,
+ new Dictionary(),
+ null),
+ };
+ var result = PublishedContentFactory.ToIPublishedMedia(contentCacheNode);
+ Assert.IsNotNull(result);
+ Assert.AreEqual(media.Id, result.Id);
+ Assert.AreEqual(media.Name, result.Name);
+
+ // Verify that requesting the same content again returns the same instance (from request cache).
+ var result2 = PublishedContentFactory.ToIPublishedMedia(contentCacheNode);
+ Assert.AreSame(result, result2);
+ }
+
+ [Test]
+ public async Task Can_Create_Published_Member_For_Member()
+ {
+ var memberType = new MemberTypeBuilder().Build();
+ await MemberTypeService.CreateAsync(memberType, Constants.Security.SuperUserKey);
+
+ var member = new MemberBuilder()
+ .WithMemberType(memberType)
+ .WithName("Member 1")
+ .Build();
+ MemberService.Save(member);
+
+ var result = PublishedContentFactory.ToPublishedMember(member);
+ Assert.IsNotNull(result);
+ Assert.AreEqual(member.Id, result.Id);
+ Assert.AreEqual(member.Name, result.Name);
+
+ // Verify that requesting the same content again returns the same instance (from request cache).
+ var result2 = PublishedContentFactory.ToPublishedMember(member);
+ Assert.AreSame(result, result2);
+ }
+}