2025-08-12 11:58:41 +02:00
#if DEBUG
using System.Diagnostics ;
#endif
2025-04-14 17:45:57 +02:00
using Microsoft.Extensions.Caching.Hybrid ;
2025-08-12 11:58:41 +02:00
using Microsoft.Extensions.Logging ;
2024-09-24 09:39:23 +02:00
using Microsoft.Extensions.Options ;
2024-09-10 00:49:18 +09:00
using Umbraco.Cms.Core ;
using Umbraco.Cms.Core.Models ;
using Umbraco.Cms.Core.Models.PublishedContent ;
2024-10-28 15:31:39 +01:00
using Umbraco.Cms.Core.PublishedCache ;
2024-09-10 00:49:18 +09:00
using Umbraco.Cms.Core.Scoping ;
using Umbraco.Cms.Core.Services ;
2025-02-17 12:51:33 +01:00
using Umbraco.Cms.Core.Services.Navigation ;
2025-08-12 11:58:41 +02:00
using Umbraco.Cms.Infrastructure.HybridCache.Extensions ;
2024-09-10 00:49:18 +09:00
using Umbraco.Cms.Infrastructure.HybridCache.Factories ;
using Umbraco.Cms.Infrastructure.HybridCache.Persistence ;
2024-10-02 13:36:26 +02:00
using Umbraco.Cms.Infrastructure.HybridCache.Serialization ;
2024-09-24 09:39:23 +02:00
using Umbraco.Extensions ;
2024-09-10 00:49:18 +09:00
namespace Umbraco.Cms.Infrastructure.HybridCache.Services ;
internal sealed class DocumentCacheService : IDocumentCacheService
{
private readonly IDatabaseCacheRepository _databaseCacheRepository ;
private readonly IIdKeyMap _idKeyMap ;
private readonly ICoreScopeProvider _scopeProvider ;
private readonly Microsoft . Extensions . Caching . Hybrid . HybridCache _hybridCache ;
private readonly IPublishedContentFactory _publishedContentFactory ;
private readonly ICacheNodeFactory _cacheNodeFactory ;
2024-09-24 09:39:23 +02:00
private readonly IEnumerable < IDocumentSeedKeyProvider > _seedKeyProviders ;
private readonly IPublishedModelFactory _publishedModelFactory ;
2024-10-03 15:35:17 +02:00
private readonly IPreviewService _previewService ;
2025-02-17 12:51:33 +01:00
private readonly IPublishStatusQueryService _publishStatusQueryService ;
2024-11-11 12:52:11 +01:00
private readonly CacheSettings _cacheSettings ;
2025-08-12 11:58:41 +02:00
private readonly ILogger < DocumentCacheService > _logger ;
2024-09-24 09:39:23 +02:00
private HashSet < Guid > ? _seedKeys ;
2025-02-17 12:51:33 +01:00
2024-09-24 09:39:23 +02:00
private HashSet < Guid > SeedKeys
{
get
{
if ( _seedKeys is not null )
{
return _seedKeys ;
}
_seedKeys = [ ] ;
foreach ( IDocumentSeedKeyProvider provider in _seedKeyProviders )
{
_seedKeys . UnionWith ( provider . GetSeedKeys ( ) ) ;
}
return _seedKeys ;
}
}
2024-09-10 00:49:18 +09:00
public DocumentCacheService (
IDatabaseCacheRepository databaseCacheRepository ,
IIdKeyMap idKeyMap ,
ICoreScopeProvider scopeProvider ,
Microsoft . Extensions . Caching . Hybrid . HybridCache hybridCache ,
IPublishedContentFactory publishedContentFactory ,
2024-09-24 09:39:23 +02:00
ICacheNodeFactory cacheNodeFactory ,
IEnumerable < IDocumentSeedKeyProvider > seedKeyProviders ,
2024-11-11 12:52:11 +01:00
IOptions < CacheSettings > cacheSettings ,
2024-10-03 15:35:17 +02:00
IPublishedModelFactory publishedModelFactory ,
2025-02-17 12:51:33 +01:00
IPreviewService previewService ,
IPublishStatusQueryService publishStatusQueryService ,
2025-08-12 11:58:41 +02:00
ILogger < DocumentCacheService > logger )
2024-09-10 00:49:18 +09:00
{
_databaseCacheRepository = databaseCacheRepository ;
_idKeyMap = idKeyMap ;
_scopeProvider = scopeProvider ;
_hybridCache = hybridCache ;
_publishedContentFactory = publishedContentFactory ;
_cacheNodeFactory = cacheNodeFactory ;
2024-09-24 09:39:23 +02:00
_seedKeyProviders = seedKeyProviders ;
_publishedModelFactory = publishedModelFactory ;
2024-10-03 15:35:17 +02:00
_previewService = previewService ;
2025-02-17 12:51:33 +01:00
_publishStatusQueryService = publishStatusQueryService ;
2024-11-11 12:52:11 +01:00
_cacheSettings = cacheSettings . Value ;
2025-08-12 11:58:41 +02:00
_logger = logger ;
2024-09-10 00:49:18 +09:00
}
2024-10-03 15:35:17 +02:00
public async Task < IPublishedContent ? > GetByKeyAsync ( Guid key , bool? preview = null )
2024-09-10 00:49:18 +09:00
{
2024-10-03 15:35:17 +02:00
bool calculatedPreview = preview ? ? GetPreview ( ) ;
2024-12-06 13:20:57 +01:00
return await GetNodeAsync ( key , calculatedPreview ) ;
2024-10-03 15:35:17 +02:00
}
public async Task < IPublishedContent ? > GetByIdAsync ( int id , bool? preview = null )
2024-09-10 00:49:18 +09:00
{
Attempt < Guid > keyAttempt = _idKeyMap . GetKeyForId ( id , UmbracoObjectTypes . Document ) ;
if ( keyAttempt . Success is false )
{
return null ;
}
2024-10-03 15:35:17 +02:00
bool calculatedPreview = preview ? ? GetPreview ( ) ;
2024-10-28 15:31:39 +01:00
Guid key = keyAttempt . Result ;
2024-10-03 15:35:17 +02:00
2024-12-06 13:20:57 +01:00
return await GetNodeAsync ( key , calculatedPreview ) ;
}
private async Task < IPublishedContent ? > GetNodeAsync ( Guid key , bool preview )
{
var cacheKey = GetCacheKey ( key , preview ) ;
2024-09-10 00:49:18 +09:00
ContentCacheNode ? contentCacheNode = await _hybridCache . GetOrCreateAsync (
2024-12-06 13:20:57 +01:00
cacheKey ,
async cancel = >
{
using ICoreScope scope = _scopeProvider . CreateCoreScope ( ) ;
ContentCacheNode ? contentCacheNode = await _databaseCacheRepository . GetContentSourceAsync ( key , preview ) ;
2025-02-17 12:51:33 +01:00
// If we can resolve the content cache node, we still need to check if the ancestor path is published.
// This does cost some performance, but it's necessary to ensure that the content is actually published.
// When unpublishing a node, a payload with RefreshBranch is published, so we don't have to worry about this.
// Similarly, when a branch is published, next time the content is requested, the parent will be published,
// this works because we don't cache null values.
2025-04-24 21:07:40 +02:00
if ( preview is false & & contentCacheNode is not null & & _publishStatusQueryService . HasPublishedAncestorPath ( contentCacheNode . Key ) is false )
2025-02-17 12:51:33 +01:00
{
2025-04-14 17:45:57 +02:00
// Careful not to early return here. We need to complete the scope even if returning null.
contentCacheNode = null ;
2025-02-17 12:51:33 +01:00
}
2024-12-06 13:20:57 +01:00
scope . Complete ( ) ;
return contentCacheNode ;
} ,
2025-04-23 10:08:08 +02:00
GetEntryOptions ( key , preview ) ,
GenerateTags ( key ) ) ;
2024-12-06 13:20:57 +01:00
if ( contentCacheNode is null )
2024-10-28 12:10:38 +01:00
{
2024-12-06 13:20:57 +01:00
return null ;
}
return _publishedContentFactory . ToIPublishedContent ( contentCacheNode , preview ) . CreateModel ( _publishedModelFactory ) ;
}
2024-10-28 12:10:38 +01:00
2025-04-23 10:08:08 +02:00
private bool GetPreview ( ) = > _previewService . IsInPreview ( ) ;
2024-09-10 00:49:18 +09:00
2024-10-01 15:03:02 +02:00
public IEnumerable < IPublishedContent > GetByContentType ( IPublishedContentType contentType )
{
using ICoreScope scope = _scopeProvider . CreateCoreScope ( ) ;
2024-10-02 13:36:26 +02:00
IEnumerable < ContentCacheNode > nodes = _databaseCacheRepository . GetContentByContentTypeKey ( [ contentType . Key ] , ContentCacheDataSerializerEntityType . Document ) ;
2024-10-01 15:03:02 +02:00
scope . Complete ( ) ;
return nodes
. Select ( x = > _publishedContentFactory . ToIPublishedContent ( x , x . IsDraft ) . CreateModel ( _publishedModelFactory ) )
. WhereNotNull ( ) ;
}
2024-10-28 15:31:39 +01:00
public async Task ClearMemoryCacheAsync ( CancellationToken cancellationToken )
{
2025-04-23 10:08:08 +02:00
await _hybridCache . RemoveByTagAsync ( Constants . Cache . Tags . Content , cancellationToken ) ;
2024-10-28 15:31:39 +01:00
// We have to run seeding again after the cache is cleared
await SeedAsync ( cancellationToken ) ;
}
public async Task RefreshMemoryCacheAsync ( Guid key )
{
using ICoreScope scope = _scopeProvider . CreateCoreScope ( ) ;
ContentCacheNode ? draftNode = await _databaseCacheRepository . GetContentSourceAsync ( key , true ) ;
if ( draftNode is not null )
{
2025-04-23 10:08:08 +02:00
await _hybridCache . SetAsync ( GetCacheKey ( draftNode . Key , true ) , draftNode , GetEntryOptions ( draftNode . Key , true ) , GenerateTags ( key ) ) ;
2024-10-28 15:31:39 +01:00
}
ContentCacheNode ? publishedNode = await _databaseCacheRepository . GetContentSourceAsync ( key , false ) ;
2025-04-24 21:07:40 +02:00
if ( publishedNode is not null & & _publishStatusQueryService . HasPublishedAncestorPath ( publishedNode . Key ) )
2024-10-28 15:31:39 +01:00
{
2025-04-23 10:08:08 +02:00
await _hybridCache . SetAsync ( GetCacheKey ( publishedNode . Key , false ) , publishedNode , GetEntryOptions ( publishedNode . Key , false ) , GenerateTags ( key ) ) ;
2024-10-28 15:31:39 +01:00
}
scope . Complete ( ) ;
}
public async Task RemoveFromMemoryCacheAsync ( Guid key )
{
await _hybridCache . RemoveAsync ( GetCacheKey ( key , true ) ) ;
await _hybridCache . RemoveAsync ( GetCacheKey ( key , false ) ) ;
}
2024-09-24 09:39:23 +02:00
public async Task SeedAsync ( CancellationToken cancellationToken )
2024-09-10 00:49:18 +09:00
{
2025-08-12 11:58:41 +02:00
#if DEBUG
var sw = new Stopwatch ( ) ;
sw . Start ( ) ;
#endif
2025-08-13 09:12:16 +01:00
foreach ( IEnumerable < Guid > group in SeedKeys . InGroupsOf ( _cacheSettings . DocumentSeedBatchSize ) )
2024-09-10 00:49:18 +09:00
{
2025-08-12 11:58:41 +02:00
var uncachedKeys = new HashSet < Guid > ( ) ;
foreach ( Guid key in group )
2024-09-10 00:49:18 +09:00
{
2025-08-12 11:58:41 +02:00
if ( cancellationToken . IsCancellationRequested )
{
break ;
}
2024-09-10 00:49:18 +09:00
2025-08-12 11:58:41 +02:00
var cacheKey = GetCacheKey ( key , false ) ;
2024-09-10 00:49:18 +09:00
2025-10-21 09:57:29 +02:00
var existsInCache = await _hybridCache . ExistsAsync < ContentCacheNode ? > ( cacheKey , cancellationToken ) . ConfigureAwait ( false ) ;
2025-08-12 11:58:41 +02:00
if ( existsInCache is false )
2025-04-23 10:08:08 +02:00
{
2025-08-12 11:58:41 +02:00
uncachedKeys . Add ( key ) ;
}
}
_logger . LogDebug ( "Uncached key count {KeyCount}" , uncachedKeys . Count ) ;
if ( uncachedKeys . Count = = 0 )
{
continue ;
}
2024-09-24 09:39:23 +02:00
2025-08-12 11:58:41 +02:00
using ICoreScope scope = _scopeProvider . CreateCoreScope ( ) ;
2024-10-28 12:10:38 +01:00
2025-08-12 11:58:41 +02:00
IEnumerable < ContentCacheNode > cacheNodes = await _databaseCacheRepository . GetContentSourcesAsync ( uncachedKeys ) ;
2024-10-28 12:10:38 +01:00
2025-08-12 11:58:41 +02:00
scope . Complete ( ) ;
2024-09-24 09:39:23 +02:00
2025-08-12 11:58:41 +02:00
_logger . LogDebug ( "Document nodes to cache {NodeCount}" , cacheNodes . Count ( ) ) ;
2025-04-23 10:08:08 +02:00
2025-08-12 11:58:41 +02:00
foreach ( ContentCacheNode cacheNode in cacheNodes )
2024-09-24 09:39:23 +02:00
{
2025-08-12 11:58:41 +02:00
var cacheKey = GetCacheKey ( cacheNode . Key , false ) ;
await _hybridCache . SetAsync (
cacheKey ,
cacheNode ,
GetSeedEntryOptions ( ) ,
GenerateTags ( cacheNode . Key ) ,
cancellationToken : cancellationToken ) ;
2024-09-24 09:39:23 +02:00
}
2024-09-10 00:49:18 +09:00
}
2025-08-12 11:58:41 +02:00
#if DEBUG
sw . Stop ( ) ;
_logger . LogInformation ( "Document cache seeding completed in {ElapsedMilliseconds} ms with {SeedCount} seed keys." , sw . ElapsedMilliseconds , SeedKeys . Count ) ;
#else
_logger . LogInformation ( "Document cache seeding completed with {SeedCount} seed keys." , SeedKeys . Count ) ;
#endif
2024-09-10 00:49:18 +09:00
}
2025-02-17 12:51:33 +01:00
// Internal for test purposes.
internal void ResetSeedKeys ( ) = > _seedKeys = null ;
2024-09-24 09:39:23 +02:00
private HybridCacheEntryOptions GetSeedEntryOptions ( ) = > new ( )
{
2024-11-11 12:52:11 +01:00
Expiration = _cacheSettings . Entry . Document . SeedCacheDuration ,
LocalCacheExpiration = _cacheSettings . Entry . Document . SeedCacheDuration
2024-09-24 09:39:23 +02:00
} ;
2025-02-26 22:48:03 +01:00
private HybridCacheEntryOptions GetEntryOptions ( Guid key , bool preview )
2024-10-28 15:31:39 +01:00
{
2025-02-26 22:48:03 +01:00
if ( SeedKeys . Contains ( key ) & & preview is false )
2024-10-28 15:31:39 +01:00
{
return GetSeedEntryOptions ( ) ;
}
return new HybridCacheEntryOptions
{
2024-11-11 12:52:11 +01:00
Expiration = _cacheSettings . Entry . Document . RemoteCacheDuration ,
LocalCacheExpiration = _cacheSettings . Entry . Document . LocalCacheDuration ,
2024-10-28 15:31:39 +01:00
} ;
}
2024-09-10 00:49:18 +09:00
public async Task < bool > HasContentByIdAsync ( int id , bool preview = false )
{
2025-04-23 10:08:08 +02:00
Attempt < Guid > keyAttempt = _idKeyMap . GetKeyForId ( id , UmbracoObjectTypes . Document ) ;
2024-09-10 00:49:18 +09:00
if ( keyAttempt . Success is false )
{
return false ;
}
2025-10-21 09:57:29 +02:00
return await _hybridCache . ExistsAsync < ContentCacheNode ? > ( GetCacheKey ( keyAttempt . Result , preview ) , CancellationToken . None ) ;
2024-09-10 00:49:18 +09:00
}
public async Task RefreshContentAsync ( IContent content )
{
using ICoreScope scope = _scopeProvider . CreateCoreScope ( ) ;
// Always set draft node
// We have nodes seperate in the cache, cause 99% of the time, you are only using one
// and thus we won't get too much data when retrieving from the cache.
2025-04-23 10:08:08 +02:00
var draftCacheNode = _cacheNodeFactory . ToContentCacheNode ( content , true ) ;
2024-09-24 09:39:23 +02:00
2024-09-10 00:49:18 +09:00
await _databaseCacheRepository . RefreshContentAsync ( draftCacheNode , content . PublishedState ) ;
2024-11-04 12:27:47 +01:00
if ( content . PublishedState = = PublishedState . Publishing | | content . PublishedState = = PublishedState . Unpublishing )
2024-09-10 00:49:18 +09:00
{
var publishedCacheNode = _cacheNodeFactory . ToContentCacheNode ( content , false ) ;
2024-09-24 09:39:23 +02:00
2024-09-10 00:49:18 +09:00
await _databaseCacheRepository . RefreshContentAsync ( publishedCacheNode , content . PublishedState ) ;
2024-11-04 12:27:47 +01:00
if ( content . PublishedState = = PublishedState . Unpublishing )
{
await _hybridCache . RemoveAsync ( GetCacheKey ( publishedCacheNode . Key , false ) ) ;
}
2024-09-10 00:49:18 +09:00
}
scope . Complete ( ) ;
}
2025-04-23 10:08:08 +02:00
private static string GetCacheKey ( Guid key , bool preview ) = > preview ? $"{key}+draft" : $"{key}" ;
// Generates the cache tags for a given CacheNode
// We use the tags to be able to clear all cache entries that are related to a given content item.
// Tags for now are only content/media, but can be expanded with draft/published later.
private static HashSet < string > GenerateTags ( Guid ? key ) = > key is null ? [ ] : [ Constants . Cache . Tags . Content ] ;
2024-09-10 00:49:18 +09:00
2024-09-24 09:39:23 +02:00
public async Task DeleteItemAsync ( IContentBase content )
2024-09-10 00:49:18 +09:00
{
using ICoreScope scope = _scopeProvider . CreateCoreScope ( ) ;
2024-09-24 09:39:23 +02:00
await _databaseCacheRepository . DeleteContentItemAsync ( content . Id ) ;
2024-09-10 00:49:18 +09:00
scope . Complete ( ) ;
}
2024-10-02 13:36:26 +02:00
public void Rebuild ( IReadOnlyCollection < int > contentTypeIds )
2024-09-10 00:49:18 +09:00
{
using ICoreScope scope = _scopeProvider . CreateCoreScope ( ) ;
2024-10-02 13:36:26 +02:00
_databaseCacheRepository . Rebuild ( contentTypeIds . ToList ( ) ) ;
2024-10-28 15:31:39 +01:00
RebuildMemoryCacheByContentTypeAsync ( contentTypeIds ) . GetAwaiter ( ) . GetResult ( ) ;
scope . Complete ( ) ;
}
public async Task RebuildMemoryCacheByContentTypeAsync ( IEnumerable < int > contentTypeIds )
{
using ICoreScope scope = _scopeProvider . CreateCoreScope ( ) ;
2024-10-02 13:36:26 +02:00
IEnumerable < ContentCacheNode > contentByContentTypeKey = _databaseCacheRepository . GetContentByContentTypeKey ( contentTypeIds . Select ( x = > _idKeyMap . GetKeyForId ( x , UmbracoObjectTypes . DocumentType ) . Result ) , ContentCacheDataSerializerEntityType . Document ) ;
2024-10-28 12:10:38 +01:00
scope . Complete ( ) ;
2024-09-10 00:49:18 +09:00
foreach ( ContentCacheNode content in contentByContentTypeKey )
{
_hybridCache . RemoveAsync ( GetCacheKey ( content . Key , true ) ) . GetAwaiter ( ) . GetResult ( ) ;
if ( content . IsDraft is false )
{
_hybridCache . RemoveAsync ( GetCacheKey ( content . Key , false ) ) . GetAwaiter ( ) . GetResult ( ) ;
}
}
}
}