diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
index 6c626efb2c5e..0cb3c52ae287 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
@@ -11,6 +11,17 @@
namespace Umbraco.Cms.Core.Cache;
+///
+/// Provides cache refresh functionality for content items, ensuring that content-related caches are updated or
+/// invalidated in response to content changes.
+///
+///
+/// The ContentCacheRefresher coordinates cache invalidation for content, including memory caches, URL
+/// caches, navigation structures, and domain assignments. It responds to content change notifications and ensures that
+/// all relevant caches reflect the current state of published and unpublished content. This refresher is used
+/// internally to maintain cache consistency after content operations such as publish, unpublish, move,
+/// or delete.
+///
public sealed class ContentCacheRefresher : PayloadCacheRefresherBase
{
@@ -25,6 +36,9 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase
+ /// Initializes a new instance of the class.
+ ///
public ContentCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
@@ -60,11 +74,15 @@ public ContentCacheRefresher(
#region Indirect
+ ///
+ /// Clears cached content and public access data from the provided application caches.
+ ///
+ /// The application caches instance from which to clear content and public access entries.
public static void RefreshContentTypes(AppCaches appCaches)
{
- // we could try to have a mechanism to notify the PublishedCachesService
+ // We could try to have a mechanism to notify the PublishedCachesService
// and figure out whether published items were modified or not... keep it
- // simple for now, just clear the whole thing
+ // simple for now, just clear the whole thing.
appCaches.ClearPartialViewCache();
appCaches.IsolatedCaches.ClearCache();
@@ -75,16 +93,22 @@ public static void RefreshContentTypes(AppCaches appCaches)
#region Define
+ ///
+ /// Represents a unique identifier for the cache refresher.
+ ///
public static readonly Guid UniqueId = Guid.Parse("900A4FBE-DF3C-41E6-BB77-BE896CD158EA");
+ ///
public override Guid RefresherUniqueId => UniqueId;
+ ///
public override string Name => "ContentCacheRefresher";
#endregion
#region Refresher
+ ///
public override void RefreshInternal(JsonPayload[] payloads)
{
AppCaches.RuntimeCache.ClearOfType();
@@ -121,13 +145,15 @@ public override void RefreshInternal(JsonPayload[] payloads)
base.RefreshInternal(payloads);
}
+ ///
public override void Refresh(JsonPayload[] payloads)
{
var idsRemoved = new HashSet();
foreach (JsonPayload payload in payloads)
{
- // if the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content
+ // If the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content.
+ // So track the IDs that have been removed.
if (payload.Blueprint is false && payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove))
{
idsRemoved.Add(payload.Id);
@@ -138,43 +164,18 @@ public override void Refresh(JsonPayload[] payloads)
HandleNavigation(payload);
HandlePublishedAsync(payload, CancellationToken.None).GetAwaiter().GetResult();
- if (payload.Id != default)
- {
- _idKeyMap.ClearCache(payload.Id);
- }
- if (payload.Key.HasValue)
- {
- _idKeyMap.ClearCache(payload.Key.Value);
- }
+ HandleIdKeyMap(payload);
}
- // Clear partial view cache when published content changes
+ // Clear partial view cache when published content changes.
if (ShouldClearPartialViewCache(payloads))
{
AppCaches.ClearPartialViewCache();
}
- if (idsRemoved.Count > 0)
- {
- var assignedDomains = _domainService.GetAll(true)
- ?.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value)).ToList();
-
- if (assignedDomains?.Count > 0)
- {
- // TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container,
- // and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the
- // DomainCacheRefresher?
- ClearAllIsolatedCacheByEntityType();
-
- // note: must do what's above FIRST else the repositories still have the old cached
- // content and when the PublishedCachesService is notified of changes it does not see
- // the new content...
- // notify
- _domainCacheService.Refresh(assignedDomains
- .Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray());
- }
- }
+ // Clear the domain cache if any domain is assigned to removed content.
+ HandleDomainCache(idsRemoved);
base.Refresh(payloads);
}
@@ -251,18 +252,51 @@ private void HandleMemoryCache(JsonPayload payload)
}
}
- private bool IsBranchUnpublished(JsonPayload payload)
- {
+ private static bool IsBranchUnpublished(JsonPayload payload) =>
+
// If unpublished cultures has one or more values, but published cultures does not, this means that the branch is unpublished entirely
// And therefore should no longer be resolve-able from the cache, so we need to remove it instead.
// Otherwise, some culture is still published, so it should be resolve-able from cache, and published cultures should instead be used.
- return payload.UnpublishedCultures is not null && payload.UnpublishedCultures.Length != 0 &&
+ payload.UnpublishedCultures is not null && payload.UnpublishedCultures.Length != 0 &&
(payload.PublishedCultures is null || payload.PublishedCultures.Length == 0);
+
+ private void HandleRouting(JsonPayload payload)
+ {
+ if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
+ {
+ Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
+
+ // Note that we need to clear the navigation service as the last thing.
+ if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(key, out IEnumerable? descendantsOrSelfKeys))
+ {
+ _documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeys).GetAwaiter().GetResult();
+ }
+ else if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeysInBin(key, out IEnumerable? descendantsOrSelfKeysInBin))
+ {
+ _documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeysInBin).GetAwaiter().GetResult();
+ }
+ }
+
+ if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
+ {
+ _documentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); // TODO: make async
+ }
+
+ if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
+ {
+ Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
+ _documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult();
+ }
+
+ if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
+ {
+ Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
+ _documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult();
+ }
}
private void HandleNavigation(JsonPayload payload)
{
-
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
{
_documentNavigationManagementService.RebuildAsync().GetAwaiter().GetResult();
@@ -389,50 +423,68 @@ private async Task HandlePublishedAsync(JsonPayload payload, CancellationToken c
await _publishStatusManagementService.AddOrUpdateStatusWithDescendantsAsync(payload.Key.Value, cancellationToken);
}
}
- private void HandleRouting(JsonPayload payload)
+
+ private void HandleIdKeyMap(JsonPayload payload)
{
- if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
+ // We only need to flush the ID/Key map when content is deleted.
+ if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove) is false)
{
- var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
-
- //Note the we need to clear the navigation service as the last thing
- if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(key, out var descendantsOrSelfKeys))
- {
- _documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeys).GetAwaiter().GetResult();
- }
- else if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeysInBin(key, out var descendantsOrSelfKeysInBin))
- {
- _documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeysInBin).GetAwaiter().GetResult();
- }
+ return;
+ }
+ if (payload.Id != default)
+ {
+ _idKeyMap.ClearCache(payload.Id);
}
- if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
+
+ if (payload.Key.HasValue)
{
- _documentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); //TODO make async
+ _idKeyMap.ClearCache(payload.Key.Value);
}
+ }
- if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
+ private void HandleDomainCache(HashSet idsRemoved)
+ {
+ if (idsRemoved.Count == 0)
{
- var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
- _documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult();
+ return;
}
- if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
+#pragma warning disable CS0618 // Type or member is obsolete
+ var assignedDomains = _domainService.GetAll(true)
+ .Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value))
+ .ToList();
+#pragma warning restore CS0618 // Type or member is obsolete
+ if (assignedDomains.Count <= 0)
{
- var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
- _documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult();
+ return;
}
+ // TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container,
+ // and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the
+ // DomainCacheRefresher?
+ ClearAllIsolatedCacheByEntityType();
+
+ // note: must do what's above FIRST else the repositories still have the old cached
+ // content and when the PublishedCachesService is notified of changes it does not see
+ // the new content...
+ // notify
+ _domainCacheService.Refresh(assignedDomains
+ .Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray());
}
- // these events should never trigger
- // everything should be PAYLOAD/JSON
+ // These events should never trigger. Everything should be PAYLOAD/JSON.
+
+ ///
public override void RefreshAll() => throw new NotSupportedException();
+ ///
public override void Refresh(int id) => throw new NotSupportedException();
+ ///
public override void Refresh(Guid id) => throw new NotSupportedException();
+ ///
public override void Remove(int id) => throw new NotSupportedException();
#endregion
@@ -440,19 +492,40 @@ private void HandleRouting(JsonPayload payload)
#region Json
// TODO (V14): Change into a record
+ ///
+ /// Represents a JSON-serializable payload containing information about a content or tree change event, including
+ /// identifiers, change types, and culture-specific publishing details.
+ ///
public class JsonPayload
{
-
+ ///
+ /// Gets the unique integer identifier for the entity.
+ ///
public int Id { get; init; }
+ ///
+ /// Gets the unique GUID key associated with the entity, or null if no key is assigned.
+ ///
public Guid? Key { get; init; }
+ ///
+ /// Gets the types of changes that have occurred in the tree.
+ ///
public TreeChangeTypes ChangeTypes { get; init; }
+ ///
+ /// Gets a value indicating whether the content represents a document blueprint.
+ ///
public bool Blueprint { get; init; }
+ ///
+ /// Gets the collection of culture codes in which the content is published.
+ ///
public string[]? PublishedCultures { get; init; }
+ ///
+ /// Gets the collection of culture codes for which the content has been unpublished.
+ ///
public string[]? UnpublishedCultures { get; init; }
}