2024-09-24 09:39:23 +02:00
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
using Microsoft.Extensions.Caching.Hybrid;
|
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
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;
|
|
|
|
|
|
using Umbraco.Cms.Core.Scoping;
|
|
|
|
|
|
using Umbraco.Cms.Core.Services;
|
|
|
|
|
|
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
|
|
|
|
|
|
using Umbraco.Cms.Infrastructure.HybridCache.Persistence;
|
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;
|
|
|
|
|
|
private readonly CacheSettings _cacheSettings;
|
2024-09-10 00:49:18 +09:00
|
|
|
|
|
2024-09-24 09:39:23 +02:00
|
|
|
|
private HashSet<Guid>? _seedKeys;
|
|
|
|
|
|
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,
|
|
|
|
|
|
IOptions<CacheSettings> cacheSettings,
|
|
|
|
|
|
IPublishedModelFactory publishedModelFactory)
|
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;
|
|
|
|
|
|
_cacheSettings = cacheSettings.Value;
|
2024-09-10 00:49:18 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<IPublishedContent?> GetByKeyAsync(Guid key, bool preview = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
|
|
|
|
|
|
|
|
|
|
|
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
|
|
|
|
|
|
GetCacheKey(key, preview), // Unique key to the cache entry
|
2024-09-24 09:39:23 +02:00
|
|
|
|
async cancel => await _databaseCacheRepository.GetContentSourceAsync(key, preview));
|
2024-09-10 00:49:18 +09:00
|
|
|
|
|
|
|
|
|
|
scope.Complete();
|
2024-09-24 09:39:23 +02:00
|
|
|
|
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory);
|
2024-09-10 00:49:18 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<IPublishedContent?> GetByIdAsync(int id, bool preview = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
Attempt<Guid> keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document);
|
|
|
|
|
|
if (keyAttempt.Success is false)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
|
|
|
|
|
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
|
|
|
|
|
|
GetCacheKey(keyAttempt.Result, preview), // Unique key to the cache entry
|
|
|
|
|
|
async cancel => await _databaseCacheRepository.GetContentSourceAsync(id, preview));
|
|
|
|
|
|
scope.Complete();
|
2024-09-24 09:39:23 +02:00
|
|
|
|
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory);;
|
2024-09-10 00:49:18 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-24 09:39:23 +02:00
|
|
|
|
public async Task SeedAsync(CancellationToken cancellationToken)
|
2024-09-10 00:49:18 +09:00
|
|
|
|
{
|
|
|
|
|
|
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
2024-09-24 09:39:23 +02:00
|
|
|
|
|
|
|
|
|
|
foreach (Guid key in SeedKeys)
|
2024-09-10 00:49:18 +09:00
|
|
|
|
{
|
2024-09-24 09:39:23 +02:00
|
|
|
|
if(cancellationToken.IsCancellationRequested)
|
2024-09-10 00:49:18 +09:00
|
|
|
|
{
|
2024-09-24 09:39:23 +02:00
|
|
|
|
break;
|
2024-09-10 00:49:18 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-24 09:39:23 +02:00
|
|
|
|
var cacheKey = GetCacheKey(key, false);
|
2024-09-10 00:49:18 +09:00
|
|
|
|
|
2024-09-24 09:39:23 +02:00
|
|
|
|
// We'll use GetOrCreateAsync because it may be in the second level cache, in which case we don't have to re-seed.
|
|
|
|
|
|
ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync<ContentCacheNode?>(
|
|
|
|
|
|
cacheKey,
|
|
|
|
|
|
async cancel =>
|
|
|
|
|
|
{
|
|
|
|
|
|
ContentCacheNode? cacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, false);
|
|
|
|
|
|
|
|
|
|
|
|
// We don't want to seed drafts
|
|
|
|
|
|
if (cacheNode is null || cacheNode.IsDraft)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return cacheNode;
|
|
|
|
|
|
},
|
|
|
|
|
|
GetSeedEntryOptions());
|
|
|
|
|
|
|
|
|
|
|
|
// If the value is null, it's likely because
|
|
|
|
|
|
if (cachedValue is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _hybridCache.RemoveAsync(cacheKey);
|
|
|
|
|
|
}
|
2024-09-10 00:49:18 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
scope.Complete();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-24 09:39:23 +02:00
|
|
|
|
private HybridCacheEntryOptions GetSeedEntryOptions() => new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Expiration = _cacheSettings.SeedCacheDuration,
|
|
|
|
|
|
LocalCacheExpiration = _cacheSettings.SeedCacheDuration
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-09-10 00:49:18 +09:00
|
|
|
|
public async Task<bool> HasContentByIdAsync(int id, bool preview = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
Attempt<Guid> keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document);
|
|
|
|
|
|
if (keyAttempt.Success is false)
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync<ContentCacheNode?>(
|
|
|
|
|
|
GetCacheKey(keyAttempt.Result, preview), // Unique key to the cache entry
|
|
|
|
|
|
cancel => ValueTask.FromResult<ContentCacheNode?>(null));
|
|
|
|
|
|
|
|
|
|
|
|
if (contentCacheNode is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _hybridCache.RemoveAsync(GetCacheKey(keyAttempt.Result, preview));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return contentCacheNode is not null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task RefreshContentAsync(IContent content)
|
|
|
|
|
|
{
|
|
|
|
|
|
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
|
|
|
|
|
|
2024-09-24 09:39:23 +02:00
|
|
|
|
bool isSeeded = SeedKeys.Contains(content.Key);
|
|
|
|
|
|
|
2024-09-10 00:49:18 +09:00
|
|
|
|
// 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.
|
2024-09-24 09:39:23 +02:00
|
|
|
|
ContentCacheNode draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true);
|
|
|
|
|
|
|
2024-09-10 00:49:18 +09:00
|
|
|
|
await _databaseCacheRepository.RefreshContentAsync(draftCacheNode, content.PublishedState);
|
2024-09-24 09:39:23 +02:00
|
|
|
|
_scopeProvider.Context?.Enlist($"UpdateMemoryCache_Draft_{content.Key}", completed =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if(completed is false)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
RefreshHybridCache(draftCacheNode, GetCacheKey(content.Key, true), isSeeded).GetAwaiter().GetResult();
|
|
|
|
|
|
}, 1);
|
2024-09-10 00:49:18 +09:00
|
|
|
|
|
|
|
|
|
|
if (content.PublishedState == PublishedState.Publishing)
|
|
|
|
|
|
{
|
|
|
|
|
|
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-09-24 09:39:23 +02:00
|
|
|
|
_scopeProvider.Context?.Enlist($"UpdateMemoryCache_{content.Key}", completed =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if(completed is false)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
RefreshHybridCache(publishedCacheNode, GetCacheKey(content.Key, false), isSeeded).GetAwaiter().GetResult();
|
|
|
|
|
|
}, 1);
|
2024-09-10 00:49:18 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
scope.Complete();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-24 09:39:23 +02:00
|
|
|
|
private async Task RefreshHybridCache(ContentCacheNode cacheNode, string cacheKey, bool isSeeded)
|
|
|
|
|
|
{
|
|
|
|
|
|
// If it's seeded we want it to stick around the cache for longer.
|
|
|
|
|
|
if (isSeeded)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _hybridCache.SetAsync(
|
|
|
|
|
|
cacheKey,
|
|
|
|
|
|
cacheNode,
|
|
|
|
|
|
GetSeedEntryOptions());
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
await _hybridCache.SetAsync(cacheKey, cacheNode);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-10 00:49:18 +09:00
|
|
|
|
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
await _hybridCache.RemoveAsync(GetCacheKey(content.Key, true));
|
|
|
|
|
|
await _hybridCache.RemoveAsync(GetCacheKey(content.Key, false));
|
2024-09-10 00:49:18 +09:00
|
|
|
|
scope.Complete();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Rebuild(IReadOnlyCollection<int> contentTypeKeys)
|
|
|
|
|
|
{
|
|
|
|
|
|
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
|
|
|
|
|
_databaseCacheRepository.Rebuild(contentTypeKeys.ToList());
|
|
|
|
|
|
IEnumerable<ContentCacheNode> contentByContentTypeKey = _databaseCacheRepository.GetContentByContentTypeKey(contentTypeKeys.Select(x => _idKeyMap.GetKeyForId(x, UmbracoObjectTypes.DocumentType).Result));
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
scope.Complete();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|