2025-04-24 13:15:30 +02:00
using Microsoft.Extensions.DependencyInjection ;
using Umbraco.Cms.Core.DependencyInjection ;
2021-03-12 21:48:24 +01:00
using Umbraco.Cms.Core.Events ;
2021-02-18 11:06:02 +01:00
using Umbraco.Cms.Core.Models ;
2021-05-11 14:33:49 +02:00
using Umbraco.Cms.Core.Notifications ;
2021-02-18 11:06:02 +01:00
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 ;
2024-09-27 09:12:19 +02:00
using Umbraco.Cms.Core.Services.Navigation ;
2021-02-18 11:06:02 +01:00
using Umbraco.Extensions ;
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
namespace Umbraco.Cms.Core.Cache ;
public sealed class ContentCacheRefresher : PayloadCacheRefresherBase < ContentCacheRefresherNotification ,
ContentCacheRefresher . JsonPayload >
2016-05-26 17:12:04 +02:00
{
2022-06-07 15:28:38 +02:00
private readonly IDomainService _domainService ;
2024-10-01 15:03:02 +02:00
private readonly IDomainCacheService _domainCacheService ;
2024-09-27 09:12:19 +02:00
private readonly IDocumentUrlService _documentUrlService ;
private readonly IDocumentNavigationQueryService _documentNavigationQueryService ;
2024-09-30 16:43:05 +02:00
private readonly IDocumentNavigationManagementService _documentNavigationManagementService ;
private readonly IContentService _contentService ;
2024-10-28 15:31:39 +01:00
private readonly IDocumentCacheService _documentCacheService ;
2025-04-24 13:15:30 +02:00
private readonly ICacheManager _cacheManager ;
2024-10-15 19:33:23 +02:00
private readonly IPublishStatusManagementService _publishStatusManagementService ;
2022-06-07 15:28:38 +02:00
private readonly IIdKeyMap _idKeyMap ;
2025-04-24 13:15:30 +02:00
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 ,
ICacheManager cacheManager )
2022-06-07 15:28:38 +02:00
: base ( appCaches , serializer , eventAggregator , factory )
2016-05-26 17:12:04 +02:00
{
2022-06-07 15:28:38 +02:00
_idKeyMap = idKeyMap ;
_domainService = domainService ;
2024-10-01 15:03:02 +02:00
_domainCacheService = domainCacheService ;
2024-09-27 09:12:19 +02:00
_documentUrlService = documentUrlService ;
_documentNavigationQueryService = documentNavigationQueryService ;
2024-09-30 16:43:05 +02:00
_documentNavigationManagementService = documentNavigationManagementService ;
_contentService = contentService ;
2024-10-28 15:31:39 +01:00
_documentCacheService = documentCacheService ;
2024-10-15 19:33:23 +02:00
_publishStatusManagementService = publishStatusManagementService ;
2025-04-24 13:15:30 +02:00
// TODO: Ideally we should inject IElementsCache
// this interface is in infrastructure, and changing this is very breaking
// so as long as we have the cache manager, which casts the IElementsCache to a simple AppCache we might as well use that.
_cacheManager = cacheManager ;
2022-06-07 15:28:38 +02:00
}
#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
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
#region Define
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
public static readonly Guid UniqueId = Guid . Parse ( "900A4FBE-DF3C-41E6-BB77-BE896CD158EA" ) ;
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
public override Guid RefresherUniqueId = > UniqueId ;
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
public override string Name = > "ContentCacheRefresher" ;
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
#endregion
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
#region Refresher
2016-05-26 17:12:04 +02:00
2025-10-08 14:55:50 +02:00
public override void RefreshInternal ( JsonPayload [ ] payloads )
2022-06-07 15:28:38 +02:00
{
AppCaches . RuntimeCache . ClearOfType < PublicAccessEntry > ( ) ;
AppCaches . RuntimeCache . ClearByKey ( CacheKeys . ContentRecycleBinCacheKey ) ;
2025-04-24 13:15:30 +02:00
// Ideally, we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache.
// The reason for this is that we have no way to know which elements are affected by the changes or what their keys are.
// This is because currently published elements live exclusively in a JSON blob in the umbracoPropertyData table.
// This means that the only way to resolve these keys is to actually parse this data with a specific value converter, and for all cultures, which is not possible.
// If published elements become their own entities with relations, instead of just property data, we can revisit this.
_cacheManager . ElementsCache . Clear ( ) ;
2022-06-07 15:28:38 +02:00
IAppPolicyCache isolatedCache = AppCaches . IsolatedCaches . GetOrCreate < IContent > ( ) ;
2024-11-07 13:17:36 +01:00
foreach ( JsonPayload payload in payloads )
2016-05-26 17:12:04 +02:00
{
2024-11-07 13:17:36 +01:00
if ( payload . Id ! = default )
{
// By INT Id
isolatedCache . Clear ( RepositoryCacheKeys . GetKey < IContent , int > ( payload . Id ) ) ;
2024-09-27 09:12:19 +02:00
2024-11-07 13:17:36 +01:00
// By GUID Key
isolatedCache . Clear ( RepositoryCacheKeys . GetKey < IContent , Guid ? > ( payload . Key ) ) ;
}
2022-06-07 15:28:38 +02:00
// remove those that are in the branch
if ( payload . ChangeTypes . HasTypesAny ( TreeChangeTypes . RefreshBranch | TreeChangeTypes . Remove ) )
2016-05-26 17:12:04 +02:00
{
2022-06-07 15:28:38 +02:00
var pathid = "," + payload . Id + "," ;
isolatedCache . ClearOfType < IContent > ( ( k , v ) = > v . Path ? . Contains ( pathid ) ? ? false ) ;
2018-05-02 14:52:00 +10:00
}
2025-10-08 14:55:50 +02:00
}
base . RefreshInternal ( payloads ) ;
}
2018-05-02 14:52:00 +10:00
2025-10-08 14:55:50 +02:00
public override void Refresh ( JsonPayload [ ] payloads )
{
var idsRemoved = new HashSet < int > ( ) ;
foreach ( JsonPayload payload in payloads )
{
2024-02-01 09:55:09 +01:00
// 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 ) )
2018-05-02 14:52:00 +10:00
{
2022-06-07 15:28:38 +02:00
idsRemoved . Add ( payload . Id ) ;
2016-05-26 17:12:04 +02:00
}
2024-09-27 09:12:19 +02:00
2024-10-28 15:31:39 +01:00
HandleMemoryCache ( payload ) ;
2024-09-27 09:12:19 +02:00
HandleRouting ( payload ) ;
2024-09-30 16:43:05 +02:00
HandleNavigation ( payload ) ;
2024-10-15 19:33:23 +02:00
HandlePublishedAsync ( payload , CancellationToken . None ) . GetAwaiter ( ) . GetResult ( ) ;
2024-11-07 13:17:36 +01:00
if ( payload . Id ! = default )
{
_idKeyMap . ClearCache ( payload . Id ) ;
}
2024-09-27 09:12:19 +02:00
if ( payload . Key . HasValue )
{
_idKeyMap . ClearCache ( payload . Key . Value ) ;
}
2016-05-26 17:12:04 +02:00
}
2022-06-07 15:28:38 +02:00
if ( idsRemoved . Count > 0 )
{
var assignedDomains = _domainService . GetAll ( true )
? . Where ( x = > x . RootContentId . HasValue & & idsRemoved . Contains ( x . RootContentId . Value ) ) . ToList ( ) ;
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
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
2024-10-01 15:03:02 +02:00
_domainCacheService . Refresh ( assignedDomains
2022-06-07 15:28:38 +02:00
. Select ( x = > new DomainCacheRefresher . JsonPayload ( x . Id , DomainChangeTypes . Remove ) ) . ToArray ( ) ) ;
}
}
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
base . Refresh ( payloads ) ;
}
2016-05-26 17:12:04 +02:00
2024-10-28 15:31:39 +01:00
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 ) ;
2025-02-17 12:51:33 +01:00
// If the branch is unpublished, we need to remove it from cache instead of refreshing it
if ( IsBranchUnpublished ( payload ) )
2024-10-28 15:31:39 +01:00
{
2025-02-17 12:51:33 +01:00
foreach ( Guid branchKey in branchKeys )
{
_documentCacheService . RemoveFromMemoryCacheAsync ( branchKey ) . GetAwaiter ( ) . GetResult ( ) ;
}
}
else
{
foreach ( Guid branchKey in branchKeys )
{
_documentCacheService . RefreshMemoryCacheAsync ( branchKey ) . GetAwaiter ( ) . GetResult ( ) ;
}
2024-10-28 15:31:39 +01:00
}
}
}
if ( payload . ChangeTypes . HasType ( TreeChangeTypes . RefreshAll ) )
{
_documentCacheService . ClearMemoryCacheAsync ( CancellationToken . None ) . GetAwaiter ( ) . GetResult ( ) ;
}
if ( payload . ChangeTypes . HasType ( TreeChangeTypes . Remove ) )
{
_documentCacheService . RemoveFromMemoryCacheAsync ( key ) . GetAwaiter ( ) . GetResult ( ) ;
}
}
2025-02-17 12:51:33 +01:00
private 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 . PublishedCultures is null | | payload . PublishedCultures . Length = = 0 ) ;
}
2024-09-30 16:43:05 +02:00
private void HandleNavigation ( JsonPayload payload )
{
2024-10-15 19:33:23 +02:00
if ( payload . ChangeTypes . HasType ( TreeChangeTypes . RefreshAll ) )
{
_documentNavigationManagementService . RebuildAsync ( ) ;
_documentNavigationManagementService . RebuildBinAsync ( ) ;
}
2024-09-30 16:43:05 +02:00
if ( payload . Key is null )
{
return ;
}
if ( payload . ChangeTypes . HasType ( TreeChangeTypes . Remove ) )
{
_documentNavigationManagementService . MoveToBin ( payload . Key . Value ) ;
_documentNavigationManagementService . RemoveFromBin ( payload . Key . Value ) ;
}
2016-05-26 17:12:04 +02:00
2024-09-30 16:43:05 +02:00
if ( payload . ChangeTypes . HasType ( TreeChangeTypes . RefreshNode ) )
{
2024-10-15 19:33:23 +02:00
IContent ? content = _contentService . GetById ( payload . Key . Value ) ;
2024-09-30 16:43:05 +02:00
if ( content is null )
{
return ;
}
HandleNavigationForSingleContent ( content ) ;
}
if ( payload . ChangeTypes . HasType ( TreeChangeTypes . RefreshBranch ) )
{
2024-10-15 19:33:23 +02:00
IContent ? content = _contentService . GetById ( payload . Key . Value ) ;
2024-09-30 16:43:05 +02:00
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 )
{
2024-11-11 12:00:20 +01:00
_documentNavigationManagementService . Add ( content . Key , content . ContentType . Key , GetParentKey ( content ) , content . SortOrder ) ;
2024-09-30 16:43:05 +02:00
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
{
2024-10-16 10:51:42 +03:00
if ( _documentNavigationQueryService . TryGetParentKey ( content . Key , out Guid ? oldParentKey ) is false )
{
return ;
}
2024-09-30 16:43:05 +02:00
// It must have been saved. Check if parent is different
2024-10-16 10:51:42 +03:00
Guid ? newParentKey = GetParentKey ( content ) ;
if ( oldParentKey ! = newParentKey )
{
_documentNavigationManagementService . Move ( content . Key , newParentKey ) ;
}
else
2024-09-30 16:43:05 +02:00
{
2024-10-16 10:51:42 +03:00
_documentNavigationManagementService . UpdateSortOrder ( content . Key , content . SortOrder ) ;
2024-09-30 16:43:05 +02:00
}
}
}
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 _ ) ;
2024-10-15 19:33:23 +02:00
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 ) ;
}
}
2024-09-27 09:12:19 +02:00
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 ( ) ;
2024-10-02 12:20:41 +02:00
}
else if ( _documentNavigationQueryService . TryGetDescendantsKeysOrSelfKeysInBin ( key , out var descendantsOrSelfKeysInBin ) )
2024-09-27 09:12:19 +02:00
{
_documentUrlService . DeleteUrlsFromCacheAsync ( descendantsOrSelfKeysInBin ) . GetAwaiter ( ) . GetResult ( ) ;
}
}
if ( payload . ChangeTypes . HasType ( TreeChangeTypes . RefreshAll ) )
{
2024-12-05 20:56:54 +01:00
_documentUrlService . InitAsync ( false , CancellationToken . None ) . GetAwaiter ( ) . GetResult ( ) ; //TODO make async
2024-09-27 09:12:19 +02:00
}
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 ( ) ;
2024-02-01 09:55:09 +01:00
}
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
}
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
// these events should never trigger
// everything should be PAYLOAD/JSON
public override void RefreshAll ( ) = > throw new NotSupportedException ( ) ;
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
public override void Refresh ( int id ) = > throw new NotSupportedException ( ) ;
2019-10-14 15:21:00 +11:00
2022-06-07 15:28:38 +02:00
public override void Refresh ( Guid id ) = > throw new NotSupportedException ( ) ;
2019-10-14 15:21:00 +11:00
2022-06-07 15:28:38 +02:00
public override void Remove ( int id ) = > throw new NotSupportedException ( ) ;
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
#endregion
2016-05-26 17:12:04 +02:00
2022-06-07 15:28:38 +02:00
#region Json
2016-05-26 17:12:04 +02:00
2024-02-01 09:55:09 +01:00
// TODO (V14): Change into a record
2022-06-07 15:28:38 +02:00
public class JsonPayload
{
2016-05-26 17:12:04 +02:00
2024-02-01 09:55:09 +01:00
public int Id { get ; init ; }
public Guid ? Key { get ; init ; }
2016-05-26 17:12:04 +02:00
2024-02-01 09:55:09 +01:00
public TreeChangeTypes ChangeTypes { get ; init ; }
2022-06-07 15:28:38 +02:00
2024-02-01 09:55:09 +01:00
public bool Blueprint { get ; init ; }
2024-12-09 11:36:48 +01:00
public string [ ] ? PublishedCultures { get ; init ; }
public string [ ] ? UnpublishedCultures { get ; init ; }
2016-05-26 17:12:04 +02:00
}
2022-06-07 15:28:38 +02:00
#endregion
2016-05-26 17:12:04 +02:00
}