Skip to content

Commit d2a0cba

Browse files
authored
V16 cherry pick of member partial cache invalidator see #19314 (#19459)
* v16 cherry pick of member partial cache invalidator see #19314 # Resolved merge conflic in src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs * Take nullmember cacheitems into account
1 parent 5a56541 commit d2a0cba

File tree

7 files changed

+213
-22
lines changed

7 files changed

+213
-22
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
2+
3+
/// <summary>
4+
/// Defines behaviours for clearing of cached partials views that are configured to be cached individually by member.
5+
/// </summary>
6+
public interface IMemberPartialViewCacheInvalidator
7+
{
8+
/// <summary>
9+
/// Clears the partial view cache items for the specified member ids.
10+
/// </summary>
11+
/// <param name="memberIds">The member Ids to clear the cache for.</param>
12+
/// <remarks>
13+
/// Called from the <see cref="MemberCacheRefresher"/> when a member is saved or deleted.
14+
/// </remarks>
15+
void ClearPartialViewCacheItems(IEnumerable<int> memberIds);
16+
}

src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
3+
using Umbraco.Cms.Core.DependencyInjection;
14
using Umbraco.Cms.Core.Events;
25
using Umbraco.Cms.Core.Models;
36
using Umbraco.Cms.Core.Notifications;
47
using Umbraco.Cms.Core.Persistence.Repositories;
58
using Umbraco.Cms.Core.Serialization;
69
using Umbraco.Cms.Core.Services;
7-
using Umbraco.Extensions;
810

911
namespace Umbraco.Cms.Core.Cache;
1012

@@ -13,10 +15,32 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
1315
public static readonly Guid UniqueId = Guid.Parse("E285DF34-ACDC-4226-AE32-C0CB5CF388DA");
1416

1517
private readonly IIdKeyMap _idKeyMap;
18+
private readonly IMemberPartialViewCacheInvalidator _memberPartialViewCacheInvalidator;
1619

20+
[Obsolete("Use the non obsoleted contructor instead. Planned for removal in V18")]
1721
public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
18-
: base(appCaches, serializer, eventAggregator, factory) =>
22+
: this(
23+
appCaches,
24+
serializer,
25+
idKeyMap,
26+
eventAggregator,
27+
factory,
28+
StaticServiceProvider.Instance.GetRequiredService<IMemberPartialViewCacheInvalidator>())
29+
{
30+
}
31+
32+
public MemberCacheRefresher(
33+
AppCaches appCaches,
34+
IJsonSerializer serializer,
35+
IIdKeyMap idKeyMap,
36+
IEventAggregator eventAggregator,
37+
ICacheRefresherNotificationFactory factory,
38+
IMemberPartialViewCacheInvalidator memberPartialViewCacheInvalidator)
39+
: base(appCaches, serializer, eventAggregator, factory)
40+
{
1941
_idKeyMap = idKeyMap;
42+
_memberPartialViewCacheInvalidator = memberPartialViewCacheInvalidator;
43+
}
2044

2145
#region Indirect
2246

@@ -65,7 +89,8 @@ public override void Remove(int id)
6589

6690
private void ClearCache(params JsonPayload[] payloads)
6791
{
68-
AppCaches.ClearPartialViewCache();
92+
_memberPartialViewCacheInvalidator.ClearPartialViewCacheItems(payloads.Select(p => p.Id));
93+
6994
Attempt<IAppPolicyCache?> memberCache = AppCaches.IsolatedCaches.Get<IMember>();
7095

7196
foreach (JsonPayload p in payloads)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using Umbraco.Cms.Core.Cache;
2+
using Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
3+
using Umbraco.Extensions;
4+
5+
namespace Umbraco.Cms.Web.Website.Cache.PartialViewCacheInvalidators;
6+
7+
/// <summary>
8+
/// Implementation of <see cref="IMemberPartialViewCacheInvalidator"/> that only remove cached partial views
9+
/// that were cached for the specified member(s).
10+
/// </summary>
11+
public class MemberPartialViewCacheInvalidator : IMemberPartialViewCacheInvalidator
12+
{
13+
private readonly AppCaches _appCaches;
14+
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="MemberPartialViewCacheInvalidator"/> class.
17+
/// </summary>
18+
public MemberPartialViewCacheInvalidator(AppCaches appCaches) => _appCaches = appCaches;
19+
20+
/// <inheritdoc/>
21+
/// <remarks>
22+
/// Partial view cache keys follow the following format:
23+
/// [] is optional or only added if the information is available
24+
/// {} is a parameter
25+
/// "Umbraco.Web.PartialViewCacheKey{partialViewName}-[{currentThreadCultureName}-][m{memberId}-][c{contextualKey}-]"
26+
/// See <see cref="HtmlHelperRenderExtensions.CachedPartialAsync"/> for more information.
27+
/// </remarks>
28+
public void ClearPartialViewCacheItems(IEnumerable<int> memberIds)
29+
{
30+
foreach (var memberId in memberIds)
31+
{
32+
_appCaches.RuntimeCache.ClearByRegex($"{CoreCacheHelperExtensions.PartialViewCacheKey}.*-m{memberId}-*");
33+
}
34+
35+
// since it is possible to add a cache item linked to members without a member logged in, we should always clear these items.
36+
_appCaches.RuntimeCache.ClearByRegex($"{CoreCacheHelperExtensions.PartialViewCacheKey}.*-m-*");
37+
}
38+
}

src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.Extensions.DependencyInjection.Extensions;
66
using Microsoft.Extensions.Logging;
77
using Microsoft.Extensions.Options;
8+
using Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
89
using Umbraco.Cms.Core.Configuration.Models;
910
using Umbraco.Cms.Core.DependencyInjection;
1011
using Umbraco.Cms.Core.Routing;
@@ -13,6 +14,7 @@
1314
using Umbraco.Cms.Infrastructure.DependencyInjection;
1415
using Umbraco.Cms.Web.Common.Middleware;
1516
using Umbraco.Cms.Web.Common.Routing;
17+
using Umbraco.Cms.Web.Website.Cache.PartialViewCacheInvalidators;
1618
using Umbraco.Cms.Web.Website.Collections;
1719
using Umbraco.Cms.Web.Website.Models;
1820
using Umbraco.Cms.Web.Website.Routing;
@@ -73,6 +75,9 @@ public static IUmbracoBuilder AddWebsite(this IUmbracoBuilder builder)
7375
builder.Services.AddSingleton<IPublicAccessRequestHandler, PublicAccessRequestHandler>();
7476
builder.Services.AddSingleton<BasicAuthenticationMiddleware>();
7577

78+
// Partial view cache invalidators
79+
builder.Services.AddUnique<IMemberPartialViewCacheInvalidator, MemberPartialViewCacheInvalidator>();
80+
7681
builder
7782
.AddDistributedCache()
7883
.AddModelsBuilder();

src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,45 @@ public static IHtmlContent PreviewBadge(
105105
ViewDataDictionary? viewData = null,
106106
Func<object, ViewDataDictionary?, string>? contextualKeyBuilder = null)
107107
{
108-
var cacheKey = new StringBuilder(partialViewName);
108+
IUmbracoContextAccessor umbracoContextAccessor = GetRequiredService<IUmbracoContextAccessor>(htmlHelper);
109+
umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext);
110+
111+
string cacheKey = await GenerateCacheKeyForCachedPartialViewAsync(
112+
partialViewName,
113+
cacheByPage,
114+
umbracoContext,
115+
cacheByMember,
116+
cacheByMember ? GetRequiredService<IMemberManager>(htmlHelper) : null,
117+
model,
118+
viewData,
119+
contextualKeyBuilder);
120+
121+
AppCaches appCaches = GetRequiredService<AppCaches>(htmlHelper);
122+
IHostingEnvironment hostingEnvironment = GetRequiredService<IHostingEnvironment>(htmlHelper);
123+
124+
return appCaches.CachedPartialView(
125+
hostingEnvironment,
126+
umbracoContext!,
127+
htmlHelper,
128+
partialViewName,
129+
model,
130+
cacheTimeout,
131+
cacheKey.ToString(),
132+
viewData);
133+
}
134+
135+
// Internal for tests.
136+
internal static async Task<string> GenerateCacheKeyForCachedPartialViewAsync(
137+
string partialViewName,
138+
bool cacheByPage,
139+
IUmbracoContext? umbracoContext,
140+
bool cacheByMember,
141+
IMemberManager? memberManager,
142+
object model,
143+
ViewDataDictionary? viewData,
144+
Func<object, ViewDataDictionary?, string>? contextualKeyBuilder)
145+
{
146+
var cacheKey = new StringBuilder(partialViewName + "-");
109147

110148
// let's always cache by the current culture to allow variants to have different cache results
111149
var cultureName = Thread.CurrentThread.CurrentUICulture.Name;
@@ -114,24 +152,25 @@ public static IHtmlContent PreviewBadge(
114152
cacheKey.AppendFormat("{0}-", cultureName);
115153
}
116154

117-
IUmbracoContextAccessor umbracoContextAccessor = GetRequiredService<IUmbracoContextAccessor>(htmlHelper);
118-
umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext);
119-
120155
if (cacheByPage)
121156
{
122157
if (umbracoContext == null)
123158
{
124159
throw new InvalidOperationException(
125-
"Cannot cache by page if the UmbracoContext has not been initialized, this parameter can only be used in the context of an Umbraco request");
160+
"Cannot cache by page if the UmbracoContext has not been initialized, this parameter can only be used in the context of an Umbraco request.");
126161
}
127162

128163
cacheKey.AppendFormat("{0}-", umbracoContext.PublishedRequest?.PublishedContent?.Id ?? 0);
129164
}
130165

131166
if (cacheByMember)
132167
{
133-
IMemberManager memberManager =
134-
htmlHelper.ViewContext.HttpContext.RequestServices.GetRequiredService<IMemberManager>();
168+
if (memberManager == null)
169+
{
170+
throw new InvalidOperationException(
171+
"Cannot cache by member if the MemberManager is not available.");
172+
}
173+
135174
MemberIdentityUser? currentMember = await memberManager.GetCurrentMemberAsync();
136175
cacheKey.AppendFormat("m{0}-", currentMember?.Id ?? "0");
137176
}
@@ -142,18 +181,7 @@ public static IHtmlContent PreviewBadge(
142181
cacheKey.AppendFormat("c{0}-", contextualKey);
143182
}
144183

145-
AppCaches appCaches = GetRequiredService<AppCaches>(htmlHelper);
146-
IHostingEnvironment hostingEnvironment = GetRequiredService<IHostingEnvironment>(htmlHelper);
147-
148-
return appCaches.CachedPartialView(
149-
hostingEnvironment,
150-
umbracoContext!,
151-
htmlHelper,
152-
partialViewName,
153-
model,
154-
cacheTimeout,
155-
cacheKey.ToString(),
156-
viewData);
184+
return cacheKey.ToString();
157185
}
158186

159187
// public static IHtmlContent EditorFor<T>(this IHtmlHelper htmlHelper, string templateName = "", string htmlFieldName = "", object additionalViewData = null)

tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using NUnit.Framework;
1313
using Umbraco.Cms.Core;
1414
using Umbraco.Cms.Core.Cache;
15+
using Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
1516
using Umbraco.Cms.Core.Composing;
1617
using Umbraco.Cms.Core.Configuration.Models;
1718
using Umbraco.Cms.Core.DistributedLocking;
@@ -43,6 +44,8 @@ public static class UmbracoBuilderExtensions
4344
public static IUmbracoBuilder AddTestServices(this IUmbracoBuilder builder, TestHelper testHelper)
4445
{
4546
builder.Services.AddUnique(AppCaches.NoCache);
47+
builder.Services.AddUnique(Mock.Of<IMemberPartialViewCacheInvalidator>());
48+
4649
builder.Services.AddUnique(Mock.Of<IUmbracoBootPermissionChecker>());
4750
builder.Services.AddUnique(testHelper.MainDom);
4851

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System.Text.RegularExpressions;
2+
using Microsoft.AspNetCore.Mvc.ModelBinding;
3+
using Microsoft.AspNetCore.Mvc.ViewFeatures;
4+
using Moq;
5+
using NUnit.Framework;
6+
using Umbraco.Cms.Core.Cache;
7+
using Umbraco.Cms.Core.Security;
8+
using Umbraco.Cms.Core.Web;
9+
using Umbraco.Cms.Web.Website.Cache.PartialViewCacheInvalidators;
10+
using Umbraco.Extensions;
11+
12+
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Routing;
13+
14+
[TestFixture]
15+
public class MemberPartialViewCacheInvalidatorTests
16+
{
17+
[Test]
18+
public void ClearPartialViewCacheItems_Clears_ExpectedItems()
19+
{
20+
var runTimeCacheMock = new Mock<IAppPolicyCache>();
21+
runTimeCacheMock
22+
.Setup(x => x.ClearByRegex(It.IsAny<string>()))
23+
.Verifiable();
24+
var appCaches = new AppCaches(
25+
runTimeCacheMock.Object,
26+
NoAppCache.Instance,
27+
new IsolatedCaches(type => new ObjectCacheAppCache()));
28+
var memberPartialViewCacheInvalidator = new MemberPartialViewCacheInvalidator(appCaches);
29+
30+
var memberIds = new[] { 1, 2, 3 };
31+
32+
memberPartialViewCacheInvalidator.ClearPartialViewCacheItems(memberIds);
33+
34+
foreach (var memberId in memberIds)
35+
{
36+
var regex = $"Umbraco.Web.PartialViewCacheKey.*-m{memberId}-*";
37+
runTimeCacheMock
38+
.Verify(x => x.ClearByRegex(It.Is<string>(x => x == regex)), Times.Once);
39+
}
40+
}
41+
42+
[Test]
43+
public async Task ClearPartialViewCacheItems_Regex_Matches_CachedKeys()
44+
{
45+
const int MemberId = 1234;
46+
47+
var memberManagerMock = new Mock<IMemberManager>();
48+
memberManagerMock
49+
.Setup(x => x.GetCurrentMemberAsync())
50+
.ReturnsAsync(new MemberIdentityUser { Id = MemberId.ToString() });
51+
52+
var cacheKey = await HtmlHelperRenderExtensions.GenerateCacheKeyForCachedPartialViewAsync(
53+
"TestPartial.cshtml",
54+
true,
55+
Mock.Of<IUmbracoContext>(),
56+
true,
57+
memberManagerMock.Object,
58+
new TestViewModel(),
59+
new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()),
60+
null);
61+
cacheKey = CoreCacheHelperExtensions.PartialViewCacheKey + cacheKey;
62+
Assert.AreEqual("Umbraco.Web.PartialViewCacheKeyTestPartial.cshtml-en-US-0-m1234-", cacheKey);
63+
64+
var regexForMember = $"Umbraco.Web.PartialViewCacheKey.*-m{MemberId}-*";
65+
var regexMatch = Regex.IsMatch(cacheKey, regexForMember);
66+
Assert.IsTrue(regexMatch);
67+
68+
var regexForAnotherMember = $"Umbraco.Web.PartialViewCacheKey.*-m{4321}-*";
69+
regexMatch = Regex.IsMatch(cacheKey, regexForAnotherMember);
70+
Assert.IsFalse(regexMatch);
71+
}
72+
73+
private class TestViewModel
74+
{
75+
}
76+
}

0 commit comments

Comments
 (0)