Skip to content

Commit f0c5ecf

Browse files
AndyButlandCopilotnikolajlauridsen
authored
Add request caching around published content factory (#19990)
* Add request caching around published content factory. * Fixed ordering of log message parameters. Co-authored-by: Copilot <[email protected]> * Invert if to reduce nesting --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: mole <[email protected]>
1 parent 019563b commit f0c5ecf

File tree

3 files changed

+259
-15
lines changed

3 files changed

+259
-15
lines changed
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1-
using Umbraco.Cms.Core.Models;
1+
using Umbraco.Cms.Core.Models;
22
using Umbraco.Cms.Core.Models.PublishedContent;
33

44
namespace Umbraco.Cms.Infrastructure.HybridCache.Factories;
55

6+
/// <summary>
7+
/// Defines a factory to create <see cref="IPublishedContent"/> and <see cref="IPublishedMember"/> from a <see cref="ContentCacheNode"/> or <see cref="IMember"/>.
8+
/// </summary>
69
internal interface IPublishedContentFactory
710
{
11+
/// <summary>
12+
/// Converts a <see cref="ContentCacheNode"/> to an <see cref="IPublishedContent"/> if document type.
13+
/// </summary>
814
IPublishedContent? ToIPublishedContent(ContentCacheNode contentCacheNode, bool preview);
15+
16+
/// <summary>
17+
/// Converts a <see cref="ContentCacheNode"/> to an <see cref="IPublishedContent"/> of media type.
18+
/// </summary>
919
IPublishedContent? ToIPublishedMedia(ContentCacheNode contentCacheNode);
1020

21+
/// <summary>
22+
/// Converts a <see cref="IMember"/> to an <see cref="IPublishedMember"/>.
23+
/// </summary>
1124
IPublishedMember ToPublishedMember(IMember member);
1225
}

src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs

Lines changed: 98 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,61 @@
1-
using Umbraco.Cms.Core.Models;
1+
using Microsoft.Extensions.Logging;
2+
using Umbraco.Cms.Core.Cache;
3+
using Umbraco.Cms.Core.Models;
24
using Umbraco.Cms.Core.Models.PublishedContent;
35
using Umbraco.Cms.Core.PublishedCache;
6+
using Umbraco.Extensions;
47

58
namespace Umbraco.Cms.Infrastructure.HybridCache.Factories;
69

10+
/// <summary>
11+
/// Defines a factory to create <see cref="IPublishedContent"/> and <see cref="IPublishedMember"/> from a <see cref="ContentCacheNode"/> or <see cref="IMember"/>.
12+
/// </summary>
713
internal sealed class PublishedContentFactory : IPublishedContentFactory
814
{
915
private readonly IElementsCache _elementsCache;
1016
private readonly IVariationContextAccessor _variationContextAccessor;
1117
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
18+
private readonly ILogger<PublishedContentFactory> _logger;
19+
private readonly AppCaches _appCaches;
1220

13-
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="PublishedContentFactory"/> class.
23+
/// </summary>
1424
public PublishedContentFactory(
1525
IElementsCache elementsCache,
1626
IVariationContextAccessor variationContextAccessor,
17-
IPublishedContentTypeCache publishedContentTypeCache)
27+
IPublishedContentTypeCache publishedContentTypeCache,
28+
ILogger<PublishedContentFactory> logger,
29+
AppCaches appCaches)
1830
{
1931
_elementsCache = elementsCache;
2032
_variationContextAccessor = variationContextAccessor;
2133
_publishedContentTypeCache = publishedContentTypeCache;
34+
_logger = logger;
35+
_appCaches = appCaches;
2236
}
2337

38+
/// <inheritdoc/>
2439
public IPublishedContent? ToIPublishedContent(ContentCacheNode contentCacheNode, bool preview)
2540
{
26-
IPublishedContentType contentType = _publishedContentTypeCache.Get(PublishedItemType.Content, contentCacheNode.ContentTypeId);
41+
var cacheKey = $"{nameof(PublishedContentFactory)}DocumentCache_{contentCacheNode.Id}_{preview}";
42+
IPublishedContent? publishedContent = _appCaches.RequestCache.GetCacheItem<IPublishedContent?>(cacheKey);
43+
if (publishedContent is not null)
44+
{
45+
_logger.LogDebug(
46+
"Using cached IPublishedContent for document {ContentCacheNodeName} ({ContentCacheNodeId}).",
47+
contentCacheNode.Data?.Name ?? "No Name",
48+
contentCacheNode.Id);
49+
return publishedContent;
50+
}
51+
52+
_logger.LogDebug(
53+
"Creating IPublishedContent for document {ContentCacheNodeName} ({ContentCacheNodeId}).",
54+
contentCacheNode.Data?.Name ?? "No Name",
55+
contentCacheNode.Id);
56+
57+
IPublishedContentType contentType =
58+
_publishedContentTypeCache.Get(PublishedItemType.Content, contentCacheNode.ContentTypeId);
2759
var contentNode = new ContentNode(
2860
contentCacheNode.Id,
2961
contentCacheNode.Key,
@@ -34,19 +66,42 @@ public PublishedContentFactory(
3466
preview ? contentCacheNode.Data : null,
3567
preview ? null : contentCacheNode.Data);
3668

37-
IPublishedContent? model = GetModel(contentNode, preview);
69+
publishedContent = GetModel(contentNode, preview);
3870

3971
if (preview)
4072
{
41-
return model ?? GetPublishedContentAsDraft(model);
73+
publishedContent ??= GetPublishedContentAsDraft(publishedContent);
74+
}
75+
76+
if (publishedContent is not null)
77+
{
78+
_appCaches.RequestCache.Set(cacheKey, publishedContent);
4279
}
4380

44-
return model;
81+
return publishedContent;
4582
}
4683

84+
/// <inheritdoc/>
4785
public IPublishedContent? ToIPublishedMedia(ContentCacheNode contentCacheNode)
4886
{
49-
IPublishedContentType contentType = _publishedContentTypeCache.Get(PublishedItemType.Media, contentCacheNode.ContentTypeId);
87+
var cacheKey = $"{nameof(PublishedContentFactory)}MediaCache_{contentCacheNode.Id}";
88+
IPublishedContent? publishedContent = _appCaches.RequestCache.GetCacheItem<IPublishedContent?>(cacheKey);
89+
if (publishedContent is not null)
90+
{
91+
_logger.LogDebug(
92+
"Using cached IPublishedContent for media {ContentCacheNodeName} ({ContentCacheNodeId}).",
93+
contentCacheNode.Data?.Name ?? "No Name",
94+
contentCacheNode.Id);
95+
return publishedContent;
96+
}
97+
98+
_logger.LogDebug(
99+
"Creating IPublishedContent for media {ContentCacheNodeName} ({ContentCacheNodeId}).",
100+
contentCacheNode.Data?.Name ?? "No Name",
101+
contentCacheNode.Id);
102+
103+
IPublishedContentType contentType =
104+
_publishedContentTypeCache.Get(PublishedItemType.Media, contentCacheNode.ContentTypeId);
50105
var contentNode = new ContentNode(
51106
contentCacheNode.Id,
52107
contentCacheNode.Key,
@@ -57,14 +112,40 @@ public PublishedContentFactory(
57112
null,
58113
contentCacheNode.Data);
59114

60-
return GetModel(contentNode, false);
115+
publishedContent = GetModel(contentNode, false);
116+
117+
if (publishedContent is not null)
118+
{
119+
_appCaches.RequestCache.Set(cacheKey, publishedContent);
120+
}
121+
122+
return publishedContent;
61123
}
62124

125+
/// <inheritdoc/>
63126
public IPublishedMember ToPublishedMember(IMember member)
64127
{
65-
IPublishedContentType contentType = _publishedContentTypeCache.Get(PublishedItemType.Member, member.ContentTypeId);
128+
string cacheKey = $"{nameof(PublishedContentFactory)}MemberCache_{member.Id}";
129+
IPublishedMember? publishedMember = _appCaches.RequestCache.GetCacheItem<IPublishedMember?>(cacheKey);
130+
if (publishedMember is not null)
131+
{
132+
_logger.LogDebug(
133+
"Using cached IPublishedMember for member {MemberName} ({MemberId}).",
134+
member.Username,
135+
member.Id);
136+
137+
return publishedMember;
138+
}
139+
140+
_logger.LogDebug(
141+
"Creating IPublishedMember for member {MemberName} ({MemberId}).",
142+
member.Username,
143+
member.Id);
144+
145+
IPublishedContentType contentType =
146+
_publishedContentTypeCache.Get(PublishedItemType.Member, member.ContentTypeId);
66147

67-
// Members are only "mapped" never cached, so these default values are a bit wierd, but they are not used.
148+
// Members are only "mapped" never cached, so these default values are a bit weird, but they are not used.
68149
var contentData = new ContentData(
69150
member.Name,
70151
null,
@@ -85,7 +166,11 @@ public IPublishedMember ToPublishedMember(IMember member)
85166
contentType,
86167
null,
87168
contentData);
88-
return new PublishedMember(member, contentNode, _elementsCache, _variationContextAccessor);
169+
publishedMember = new PublishedMember(member, contentNode, _elementsCache, _variationContextAccessor);
170+
171+
_appCaches.RequestCache.Set(cacheKey, publishedMember);
172+
173+
return publishedMember;
89174
}
90175

91176
private static Dictionary<string, PropertyData[]> GetPropertyValues(IPublishedContentType contentType, IMember member)
@@ -134,7 +219,6 @@ private static void AddIf(IPublishedContentType contentType, IDictionary<string,
134219
_variationContextAccessor);
135220
}
136221

137-
138222
private static IPublishedContent? GetPublishedContentAsDraft(IPublishedContent? content) =>
139223
content == null ? null :
140224
// an object in the cache is either an IPublishedContentOrMedia,
@@ -149,7 +233,7 @@ private static PublishedContent UnwrapIPublishedContent(IPublishedContent conten
149233
content = wrapped.Unwrap();
150234
}
151235

152-
if (!(content is PublishedContent inner))
236+
if (content is not PublishedContent inner)
153237
{
154238
throw new InvalidOperationException("Innermost content is not PublishedContent.");
155239
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
using NUnit.Framework;
2+
using Umbraco.Cms.Core;
3+
using Umbraco.Cms.Core.Cache;
4+
using Umbraco.Cms.Core.Models.PublishedContent;
5+
using Umbraco.Cms.Core.Services;
6+
using Umbraco.Cms.Infrastructure.HybridCache;
7+
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
8+
using Umbraco.Cms.Tests.Common.Builders;
9+
using Umbraco.Cms.Tests.Common.Builders.Extensions;
10+
using Umbraco.Cms.Tests.Common.Testing;
11+
using Umbraco.Cms.Tests.Integration.Testing;
12+
13+
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
14+
15+
[TestFixture]
16+
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
17+
internal sealed class PublishedContentFactoryTests : UmbracoIntegrationTestWithContent
18+
{
19+
private IPublishedContentFactory PublishedContentFactory => GetRequiredService<IPublishedContentFactory>();
20+
21+
private IPublishedValueFallback PublishedValueFallback => GetRequiredService<IPublishedValueFallback>();
22+
23+
private IMediaService MediaService => GetRequiredService<IMediaService>();
24+
25+
private IMediaTypeService MediaTypeService => GetRequiredService<IMediaTypeService>();
26+
27+
private IMemberService MemberService => GetRequiredService<IMemberService>();
28+
29+
private IMemberTypeService MemberTypeService => GetRequiredService<IMemberTypeService>();
30+
31+
protected override void CustomTestSetup(IUmbracoBuilder builder)
32+
{
33+
var requestCache = new DictionaryAppCache();
34+
var appCaches = new AppCaches(
35+
NoAppCache.Instance,
36+
requestCache,
37+
new IsolatedCaches(type => NoAppCache.Instance));
38+
builder.Services.AddUnique(appCaches);
39+
}
40+
41+
[Test]
42+
public void Can_Create_Published_Content_For_Document()
43+
{
44+
var contentCacheNode = new ContentCacheNode
45+
{
46+
Id = Textpage.Id,
47+
Key = Textpage.Key,
48+
ContentTypeId = Textpage.ContentType.Id,
49+
CreateDate = Textpage.CreateDate,
50+
CreatorId = Textpage.CreatorId,
51+
SortOrder = Textpage.SortOrder,
52+
Data = new ContentData(
53+
Textpage.Name,
54+
"text-page",
55+
Textpage.VersionId,
56+
Textpage.UpdateDate,
57+
Textpage.WriterId,
58+
Textpage.TemplateId,
59+
true,
60+
new Dictionary<string, PropertyData[]>
61+
{
62+
{
63+
"title", new[]
64+
{
65+
new PropertyData
66+
{
67+
Value = "Test title",
68+
Culture = string.Empty,
69+
Segment = string.Empty,
70+
},
71+
}
72+
},
73+
},
74+
null),
75+
};
76+
var result = PublishedContentFactory.ToIPublishedContent(contentCacheNode, false);
77+
Assert.IsNotNull(result);
78+
Assert.AreEqual(Textpage.Id, result.Id);
79+
Assert.AreEqual(Textpage.Name, result.Name);
80+
Assert.AreEqual("Test title", result.Properties.Single(x => x.Alias == "title").Value<string>(PublishedValueFallback));
81+
82+
// Verify that requesting the same content again returns the same instance (from request cache).
83+
var result2 = PublishedContentFactory.ToIPublishedContent(contentCacheNode, false);
84+
Assert.AreSame(result, result2);
85+
}
86+
87+
[Test]
88+
public async Task Can_Create_Published_Content_For_Media()
89+
{
90+
var mediaType = new MediaTypeBuilder().Build();
91+
mediaType.AllowedAsRoot = true;
92+
await MediaTypeService.CreateAsync(mediaType, Constants.Security.SuperUserKey);
93+
94+
var media = new MediaBuilder()
95+
.WithMediaType(mediaType)
96+
.WithName("Media 1")
97+
.Build();
98+
MediaService.Save(media);
99+
100+
var contentCacheNode = new ContentCacheNode
101+
{
102+
Id = media.Id,
103+
Key = media.Key,
104+
ContentTypeId = media.ContentType.Id,
105+
Data = new ContentData(
106+
media.Name,
107+
null,
108+
0,
109+
media.UpdateDate,
110+
media.WriterId,
111+
null,
112+
false,
113+
new Dictionary<string, PropertyData[]>(),
114+
null),
115+
};
116+
var result = PublishedContentFactory.ToIPublishedMedia(contentCacheNode);
117+
Assert.IsNotNull(result);
118+
Assert.AreEqual(media.Id, result.Id);
119+
Assert.AreEqual(media.Name, result.Name);
120+
121+
// Verify that requesting the same content again returns the same instance (from request cache).
122+
var result2 = PublishedContentFactory.ToIPublishedMedia(contentCacheNode);
123+
Assert.AreSame(result, result2);
124+
}
125+
126+
[Test]
127+
public async Task Can_Create_Published_Member_For_Member()
128+
{
129+
var memberType = new MemberTypeBuilder().Build();
130+
await MemberTypeService.CreateAsync(memberType, Constants.Security.SuperUserKey);
131+
132+
var member = new MemberBuilder()
133+
.WithMemberType(memberType)
134+
.WithName("Member 1")
135+
.Build();
136+
MemberService.Save(member);
137+
138+
var result = PublishedContentFactory.ToPublishedMember(member);
139+
Assert.IsNotNull(result);
140+
Assert.AreEqual(member.Id, result.Id);
141+
Assert.AreEqual(member.Name, result.Name);
142+
143+
// Verify that requesting the same content again returns the same instance (from request cache).
144+
var result2 = PublishedContentFactory.ToPublishedMember(member);
145+
Assert.AreSame(result, result2);
146+
}
147+
}

0 commit comments

Comments
 (0)