Files
Umbraco-CMS/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs
Mole 1258962429 V15: Remove Nucache (#17166)
* Remove nucache reference from Web.Common

* Get tests building-ish

* Move ReservedFieldNamesService to the right project

* Remove IPublishedSnapshotStatus

* Added functionality to the INavigationQueryService to get root keys

* Fixed issue with navigation

* Remove IPublishedSnapshot from UmbracoContext

* Begin removing usage of IPublishedSnapshot from PublishedContentExtensions

* Fix PublishedContentExtensions.cs

* Don't use snapshots in delivery media api

* Use IPublishedMediaCache in QueryMediaApiController

* Remove more usages of IPublishedSnapshotAccessor

* Comment out tests

* Remove more usages of PublishedSnapshotAccessor

* Remove PublishedSnapshot from property

* Fixed test build

* Fix errors

* Fix some tests

* Delete NuCache 🎉

* Implement DatabaseCacheRebuilder

* Remove usage of IPublishedSnapshotService

* Remove IPublishedSnapshotService

* Remove TestPublishedSnapshotAccessor and make tests build

* Don't test Snapshot cachelevel

It's no longer supported

* Fix BlockEditorConverter

Element != Element document type

* Remember to set cachemanager

* Fix RichTextParserTests

* Implement TryGetLevel on INavigationQueryService

* Fake level and obsolete it in PublishedContent

* Remove ChildrenForAllCultures

* Hack Path property on PublishedContent

* Remove usages of IPublishedSnapshot in tests

* More ConvertersTests

* Add hybrid cache to integration tests

We can actually do this now because we no longer save files on disk

* Rename IPublishedSnapshotRebuilder to ICacheRebuilder

* Comment out tests

* V15: Replacing the usages of Parent (navigation data) from IPublishedContent (#17125)

* Fix .Parent references in PublishedContentExtensions

* Add missing methods to FriendlyPublishedContentExtensions (ones that you were able to call on the content directly as they now require extra params)

* Fix references from the extension methods

* Fix dependencies in tests

* Replace IPublishedSnapshotAccessor with the content cache in tests

* Resolving more .Parent references

* Fix unit tests

* Obsolete and use extension methods

* Remove private method and use extension instead

* Moving code around

* Fix tests

* Fix more references

* Cleanup

* Fix more usages

* Resolve merge conflict

* Fix tests

* Cleanup

* Fix more tests

* Fixed unit tests

* Cleanup

* Replace last usages

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>

* Remove usage of IPublishedSnapshotAccessor from IRequestItemProvider

* Post merge fixup

* Remo IPublishedSnapshot

* Add HasAny to IDocumentUrlService

* Fix TextBuilder

* Fix modelsbuilder tests

* Use explicit types

* Implement GetByContentType

* Support element types in PublishedContentTypeCache

* Run enlistments before publishing notifications

* Fix elements cache refreshing

* Implement GetByUdi

* Implement GetAtRoot

* Implement GetByRoute

* Reimplement GetRouteById

* Fix blocks unit tests

* Initialize domain cache on boot

* Only return routes with domains on non default lanauges

* V15: Replacing the usages of `Children` (navigation data) from `IPublishedContent` (#17159)

* Update params in PublishedContentExtensions to the general interfaces for the published cache and navigation service, so that we can use the extension methods on both documents and media

* Introduce GetParent() which uses the right services

* Fix obsolete message on .Parent

* Obsolete .Children

* Fix usages of Children for ApiMediaQueryService

* Fix usage in internal

* Fix usages in views

* Fix indentation

* Fix issue with delete language

* Update nuget pacakges

* Clear elements cache when content is deleted

instead of trying to update it

* Reset publishedModelFactory

* Fixed publishing

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com>
Co-authored-by: kjac <kja@umbraco.dk>
2024-10-01 15:03:02 +02:00

261 lines
9.7 KiB
C#

using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Options;
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;
using Umbraco.Extensions;
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;
private readonly IEnumerable<IDocumentSeedKeyProvider> _seedKeyProviders;
private readonly IPublishedModelFactory _publishedModelFactory;
private readonly CacheSettings _cacheSettings;
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;
}
}
public DocumentCacheService(
IDatabaseCacheRepository databaseCacheRepository,
IIdKeyMap idKeyMap,
ICoreScopeProvider scopeProvider,
Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache,
IPublishedContentFactory publishedContentFactory,
ICacheNodeFactory cacheNodeFactory,
IEnumerable<IDocumentSeedKeyProvider> seedKeyProviders,
IOptions<CacheSettings> cacheSettings,
IPublishedModelFactory publishedModelFactory)
{
_databaseCacheRepository = databaseCacheRepository;
_idKeyMap = idKeyMap;
_scopeProvider = scopeProvider;
_hybridCache = hybridCache;
_publishedContentFactory = publishedContentFactory;
_cacheNodeFactory = cacheNodeFactory;
_seedKeyProviders = seedKeyProviders;
_publishedModelFactory = publishedModelFactory;
_cacheSettings = cacheSettings.Value;
}
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
async cancel => await _databaseCacheRepository.GetContentSourceAsync(key, preview));
scope.Complete();
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory);
}
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();
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory);;
}
public IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
IEnumerable<ContentCacheNode> nodes = _databaseCacheRepository.GetContentByContentTypeKey([contentType.Key]);
scope.Complete();
return nodes
.Select(x => _publishedContentFactory.ToIPublishedContent(x, x.IsDraft).CreateModel(_publishedModelFactory))
.WhereNotNull();
}
public async Task SeedAsync(CancellationToken cancellationToken)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
foreach (Guid key in SeedKeys)
{
if(cancellationToken.IsCancellationRequested)
{
break;
}
var cacheKey = GetCacheKey(key, false);
// 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);
}
}
scope.Complete();
}
private HybridCacheEntryOptions GetSeedEntryOptions() => new()
{
Expiration = _cacheSettings.SeedCacheDuration,
LocalCacheExpiration = _cacheSettings.SeedCacheDuration
};
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();
bool isSeeded = SeedKeys.Contains(content.Key);
// 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.
ContentCacheNode draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true);
await _databaseCacheRepository.RefreshContentAsync(draftCacheNode, content.PublishedState);
_scopeProvider.Context?.Enlist($"UpdateMemoryCache_Draft_{content.Key}", completed =>
{
if(completed is false)
{
return;
}
RefreshHybridCache(draftCacheNode, GetCacheKey(content.Key, true), isSeeded).GetAwaiter().GetResult();
}, 1);
if (content.PublishedState == PublishedState.Publishing)
{
var publishedCacheNode = _cacheNodeFactory.ToContentCacheNode(content, false);
await _databaseCacheRepository.RefreshContentAsync(publishedCacheNode, content.PublishedState);
_scopeProvider.Context?.Enlist($"UpdateMemoryCache_{content.Key}", completed =>
{
if(completed is false)
{
return;
}
RefreshHybridCache(publishedCacheNode, GetCacheKey(content.Key, false), isSeeded).GetAwaiter().GetResult();
}, 1);
}
scope.Complete();
}
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);
}
}
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
public async Task DeleteItemAsync(IContentBase content)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
await _databaseCacheRepository.DeleteContentItemAsync(content.Id);
await _hybridCache.RemoveAsync(GetCacheKey(content.Key, true));
await _hybridCache.RemoveAsync(GetCacheKey(content.Key, false));
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();
}
}