Files
Umbraco-CMS/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
Elitsa Marinovska c26016b0b0 V15: Implement content type filtering for in-memory navigation structure (#17456)
* Adding contentType to navigation node

* Loading contentType from DB

* Considering contentTypeKey when adding a navigation node & fixing references

* Using IContentTypeBaseService to load content types

* Adding generics to ContentNavigationServiceBase and fixing references

* Adding TryGetChildrenKeysOfType and implementation

* Refactoring test data

* Adding unit tests for TryGetChildrenKeysOfType

* Update CreateContentCreateModel in tests to receive content type

* Fix references

* Cleanup

* Adding integration tests for TryGetChildrenKeysOfType

* Cleanup

* Cleanup

* Descendants of type implementation

* Descendants of type tests

* Interface updates

* Ancestors of type implementation and tests

* Siblings of type implementation and tests

* Cleanup

* Integration tests

* Adding root of type implementation and tests

* Fix Ancestors extension methods

* Fix descendants extension methods

* Fix children extension methods

* Fix siblings extension methods

* Add helper methods

* Fix a bug

* Fixed unit tests by setting up mocks

* Adding missing extension method

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2024-11-11 12:00:20 +01:00

386 lines
14 KiB
C#

using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache;
public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCacheRefresherNotification,
ContentCacheRefresher.JsonPayload>
{
private readonly IDomainService _domainService;
private readonly IDomainCacheService _domainCacheService;
private readonly IDocumentUrlService _documentUrlService;
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
private readonly IDocumentNavigationManagementService _documentNavigationManagementService;
private readonly IContentService _contentService;
private readonly IDocumentCacheService _documentCacheService;
private readonly IPublishStatusManagementService _publishStatusManagementService;
private readonly IIdKeyMap _idKeyMap;
public ContentCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IIdKeyMap idKeyMap,
IDomainService domainService,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory,
IDocumentUrlService documentUrlService,
IDomainCacheService domainCacheService,
IDocumentNavigationQueryService documentNavigationQueryService,
IDocumentNavigationManagementService documentNavigationManagementService,
IContentService contentService,
IPublishStatusManagementService publishStatusManagementService,
IDocumentCacheService documentCacheService)
: base(appCaches, serializer, eventAggregator, factory)
{
_idKeyMap = idKeyMap;
_domainService = domainService;
_domainCacheService = domainCacheService;
_documentUrlService = documentUrlService;
_documentNavigationQueryService = documentNavigationQueryService;
_documentNavigationManagementService = documentNavigationManagementService;
_contentService = contentService;
_documentCacheService = documentCacheService;
_publishStatusManagementService = publishStatusManagementService;
}
#region Indirect
public static void RefreshContentTypes(AppCaches appCaches)
{
// 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
appCaches.ClearPartialViewCache();
appCaches.IsolatedCaches.ClearCache<PublicAccessEntry>();
appCaches.IsolatedCaches.ClearCache<IContent>();
}
#endregion
#region Define
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 Refresh(JsonPayload[] payloads)
{
AppCaches.RuntimeCache.ClearOfType<PublicAccessEntry>();
AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey);
var idsRemoved = new HashSet<int>();
IAppPolicyCache isolatedCache = AppCaches.IsolatedCaches.GetOrCreate<IContent>();
foreach (JsonPayload payload in payloads)
{
if (payload.Id != default)
{
// By INT Id
isolatedCache.Clear(RepositoryCacheKeys.GetKey<IContent, int>(payload.Id));
// By GUID Key
isolatedCache.Clear(RepositoryCacheKeys.GetKey<IContent, Guid?>(payload.Key));
}
// remove those that are in the branch
if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove))
{
var pathid = "," + payload.Id + ",";
isolatedCache.ClearOfType<IContent>((k, v) => v.Path?.Contains(pathid) ?? false);
}
// 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 (payload.Blueprint is false && payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove))
{
idsRemoved.Add(payload.Id);
}
HandleMemoryCache(payload);
HandleRouting(payload);
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);
}
}
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<IDomain>();
// 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());
}
}
base.Refresh(payloads);
}
private void HandleMemoryCache(JsonPayload payload)
{
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
if (payload.Blueprint)
{
return;
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
{
_documentCacheService.RefreshMemoryCacheAsync(key).GetAwaiter().GetResult();
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
{
if (_documentNavigationQueryService.TryGetDescendantsKeys(key, out IEnumerable<Guid> descendantsKeys))
{
var branchKeys = descendantsKeys.ToList();
branchKeys.Add(key);
foreach (Guid branchKey in branchKeys)
{
_documentCacheService.RefreshMemoryCacheAsync(branchKey).GetAwaiter().GetResult();
}
}
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
{
_documentCacheService.ClearMemoryCacheAsync(CancellationToken.None).GetAwaiter().GetResult();
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
{
_documentCacheService.RemoveFromMemoryCacheAsync(key).GetAwaiter().GetResult();
}
}
private void HandleNavigation(JsonPayload payload)
{
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
{
_documentNavigationManagementService.RebuildAsync();
_documentNavigationManagementService.RebuildBinAsync();
}
if (payload.Key is null)
{
return;
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
{
_documentNavigationManagementService.MoveToBin(payload.Key.Value);
_documentNavigationManagementService.RemoveFromBin(payload.Key.Value);
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
{
IContent? content = _contentService.GetById(payload.Key.Value);
if (content is null)
{
return;
}
HandleNavigationForSingleContent(content);
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
{
IContent? content = _contentService.GetById(payload.Key.Value);
if (content is null)
{
return;
}
IEnumerable<IContent> descendants = _contentService.GetPagedDescendants(content.Id, 0, int.MaxValue, out _);
foreach (IContent descendant in content.Yield().Concat(descendants))
{
HandleNavigationForSingleContent(descendant);
}
}
}
private void HandleNavigationForSingleContent(IContent content)
{
// First creation
if (ExistsInNavigation(content.Key) is false && ExistsInNavigationBin(content.Key) is false)
{
_documentNavigationManagementService.Add(content.Key, content.ContentType.Key, GetParentKey(content), content.SortOrder);
if (content.Trashed)
{
// If created as trashed, move to bin
_documentNavigationManagementService.MoveToBin(content.Key);
}
}
else if (ExistsInNavigation(content.Key) && ExistsInNavigationBin(content.Key) is false)
{
if (content.Trashed)
{
// It must have been trashed
_documentNavigationManagementService.MoveToBin(content.Key);
}
else
{
if (_documentNavigationQueryService.TryGetParentKey(content.Key, out Guid? oldParentKey) is false)
{
return;
}
// It must have been saved. Check if parent is different
Guid? newParentKey = GetParentKey(content);
if (oldParentKey != newParentKey)
{
_documentNavigationManagementService.Move(content.Key, newParentKey);
}
else
{
_documentNavigationManagementService.UpdateSortOrder(content.Key, content.SortOrder);
}
}
}
else if (ExistsInNavigation(content.Key) is false && ExistsInNavigationBin(content.Key))
{
if (content.Trashed is false)
{
// It must have been restored
_documentNavigationManagementService.RestoreFromBin(content.Key, GetParentKey(content));
}
}
}
private Guid? GetParentKey(IContent content) => (content.ParentId == -1) ? null : _idKeyMap.GetKeyForId(content.ParentId, UmbracoObjectTypes.Document).Result;
private bool ExistsInNavigation(Guid contentKey) => _documentNavigationQueryService.TryGetParentKey(contentKey, out _);
private bool ExistsInNavigationBin(Guid contentKey) => _documentNavigationQueryService.TryGetParentKeyInBin(contentKey, out _);
private async Task HandlePublishedAsync(JsonPayload payload, CancellationToken cancellationToken)
{
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
{
await _publishStatusManagementService.InitializeAsync(cancellationToken);
}
if (payload.Key.HasValue is false)
{
return;
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
{
await _publishStatusManagementService.RemoveAsync(payload.Key.Value, cancellationToken);
}
else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
{
await _publishStatusManagementService.AddOrUpdateStatusAsync(payload.Key.Value, cancellationToken);
}
else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
{
await _publishStatusManagementService.AddOrUpdateStatusWithDescendantsAsync(payload.Key.Value, cancellationToken);
}
}
private void HandleRouting(JsonPayload payload)
{
if(payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
{
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();
}
}
if(payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
{
_documentUrlService.RebuildAllUrlsAsync().GetAwaiter().GetResult(); //TODO make async
}
if(payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
{
var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
_documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult();
}
if(payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
{
var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
_documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult();
}
}
// 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
#region Json
// TODO (V14): Change into a record
public class JsonPayload
{
public int Id { get; init; }
public Guid? Key { get; init; }
public TreeChangeTypes ChangeTypes { get; init; }
public bool Blueprint { get; init; }
}
#endregion
}