V15: Hybrid Caching (#16938)
* Update to dotnet 9 and update nuget packages * Update umbraco code version * Update Directory.Build.props Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> * Include preview version in pipeline * update template projects * update global json with specific version * Update version.json to v15 * Rename TrimStart and TrimEnd to string specific * Rename to Exact * Update global.json Co-authored-by: Ronald Barendse <ronald@barend.se> * Remove includePreviewVersion * Rename to trim exact * Add new Hybridcache project * Add tests * Start implementing PublishedContent.cs * Implement repository for content * Refactor to use async everywhere * Add cache refresher * make public as needed for serialization * Use content type cache to get content type out * Refactor to use ContentCacheNode model, that goes in the memory cache * Remove content node kit as its not needed * Implement tests for ensuring caching * Implement better asserts * Implement published property * Refactor to use mapping * Rename to document tests * Update to test properties * Create more tests * Refactor mock tests into own file * Update property test * Fix published version of content * Change default cache level to elements * Refactor to always have draft * Refactor to not use PublishedModelFactory * Added tests * Added and updated tests * Fixed tests * Don't return empty object with id * More tests * Added key * Another key * Refactor CacheService to be responsible for using the hybrid cache * Use notification handler to remove deleted content from cache * Add more tests for missing functions * Implement missing methods * Remove HasContent as it pertains to routing * Fik up test * formatting * refactor variable names * Implement variant tests * Map all the published content properties * Get item out of cache first, to assert updated * Implement member cache * Add member test * Implement media cache * Implement property tests for media tests * Refactor tests to use extension method * Add more media tests * Refactor properties to no longer have element caching * Don't use property cache level * Start implementing seeding * Only seed when main * Add Immutable for performance * Implement permanent seeding of content * Implement cache settings * Implement tests for seeding * Update package version * start refactoring nurepo * Refactor so draft & published nodes are cached individually * Refactor RefreshContent to take node instead of IContent * Refactor media to also use cache nodes * Remove member from repo as it isn't cached * Refactor media to not include preview, as media has no draft * create new benchmark project * POC Integration benchmarks with custom api controllers * Start implementing content picker tests * Implement domain cache * Rework content cache to implement interface * Start implementing elements cache * Implement published snapshot service * Publish snapshot tests * Use snapshot for elements cache * Create test proving we don't clear cache when updating content picker * Clear entire elements cache * Remove properties from element cache, when content gets updated. * Rename methods to async * Refactor to use old cache interfaces instead of new ones * Remove snapshot, as it is no longer needed * Fix tests building * Refactor domaincache to not have snapshots * Delete benchmarks * Delete benchmarks * Add HybridCacheProject to Umbraco * Add comment to route value transformer * Implement is draft * remove snapshot from property * V15 updated the hybrid caching integration tests to use ContentEditingService (#16947) * Added builder extension withParentKey * Created builder with ContentEditingService * Added usage of the ContentEditingService to SETUP * Started using ContentEditingService builder in tests * Updated builder extensions * Fixed builder * Clean up * Clean up, not done * Added Ids * Remove entries from cache on delete * Fix up seeding logic * Don't register hybrid cache twice * Change seeded entry options * Update hybrid cache package * Fix up published property to work with delivery api again * Fix dependency injection to work with tests * Fix naming * Dont make caches nullable * Make content node sealed * Remove path and other unused from content node * Remove hacky 2 phase ctor * Refactor to actually set content templates * Remove umbraco context * Remove "HasBy" methods * rename property data * Delete obsolete legacy stuff * Add todo for making expiration configurable * Add todo in UmbracoContext * Add clarifying comment in content factory * Remove xml stuff from published property * Fix according to review * Make content type cache injectible * Make content type cache injectible * Rename to database cache repository * Rename to document cache * Add TODO * Refactor to async * Rename to async * Make everything async * Remove duplicate line from json schema * Move Hybrid cache project * Remove leftover file * Refactor to use keys * Refactor published content to no longer have content data, as it is on the node itself * Refactor to member to use proper content node ctor * Move tests to own folder * Add immutable objects to property and content data for performance * Make property data public * Fix member caching to be singleton * Obsolete GetContentType * Remove todo * Fix naming * Fix lots of exposed errors due to scope test * Add final scope tests * Rename to document cache service * Rename test files * Create new doc type tests * Add ignore to tests * Start implementing refresh for content type save * Clear contenttype cache when contenttype is updated * Fix test Teh contenttype is not upated unless the property is dirty * Use init for ContentSourceDto * Fix get by key in PublishedContentTypeCache * Remove ContentType from PublishedContentTypeCache when contenttype is deleted * Update to preview 7 * Fix versions * Increase timeout for sqlite integration tests * Undo timeout increase * Try and undo init change to ContentSourceDto * That wasn't it chief * Try and make DomainAndUrlsTests non NonParallelizable * Update versions * Only run cache tests on linux for now --------- Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Co-authored-by: Ronald Barendse <ronald@barend.se> Co-authored-by: Andreas Zerbst <andr317c@live.dk> Co-authored-by: Sven Geusens <sge@umbraco.dk> Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Co-authored-by: nikolajlauridsen <nikolajlauridsen@protonmail.ch>
This commit is contained in:
27
src/Umbraco.PublishedCache.HybridCache/CacheManager.cs
Normal file
27
src/Umbraco.PublishedCache.HybridCache/CacheManager.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class CacheManager : ICacheManager
|
||||
{
|
||||
public CacheManager(IPublishedContentCache content, IPublishedMediaCache media, IPublishedMemberCache members, IDomainCache domains, IElementsCache elementsCache)
|
||||
{
|
||||
ElementsCache = elementsCache;
|
||||
Content = content;
|
||||
Media = media;
|
||||
Members = members;
|
||||
Domains = domains;
|
||||
}
|
||||
|
||||
public IPublishedContentCache Content { get; }
|
||||
|
||||
public IPublishedMediaCache Media { get; }
|
||||
|
||||
public IPublishedMemberCache Members { get; }
|
||||
|
||||
public IDomainCache Domains { get; }
|
||||
|
||||
public IAppCache ElementsCache { get; }
|
||||
}
|
||||
24
src/Umbraco.PublishedCache.HybridCache/ContentCacheNode.cs
Normal file
24
src/Umbraco.PublishedCache.HybridCache/ContentCacheNode.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
// This is for cache performance reasons, see https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0#reuse-objects
|
||||
[ImmutableObject(true)]
|
||||
internal sealed class ContentCacheNode
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public Guid Key { get; set; }
|
||||
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
public int CreatorId { get; set; }
|
||||
|
||||
public int ContentTypeId { get; set; }
|
||||
|
||||
public bool IsDraft { get; set; }
|
||||
|
||||
public ContentData? Data { get; set; }
|
||||
}
|
||||
45
src/Umbraco.PublishedCache.HybridCache/ContentData.cs
Normal file
45
src/Umbraco.PublishedCache.HybridCache/ContentData.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
/// <summary>
|
||||
/// Represents everything that is specific to an edited or published content version
|
||||
/// </summary>
|
||||
// This is for cache performance reasons, see https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0#reuse-objects
|
||||
[ImmutableObject(true)]
|
||||
internal sealed class ContentData
|
||||
{
|
||||
public ContentData(string? name, string? urlSegment, int versionId, DateTime versionDate, int writerId, int? templateId, bool published, Dictionary<string, PropertyData[]>? properties, IReadOnlyDictionary<string, CultureVariation>? cultureInfos)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
UrlSegment = urlSegment;
|
||||
VersionId = versionId;
|
||||
VersionDate = versionDate;
|
||||
WriterId = writerId;
|
||||
TemplateId = templateId;
|
||||
Published = published;
|
||||
Properties = properties ?? throw new ArgumentNullException(nameof(properties));
|
||||
CultureInfos = cultureInfos;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string? UrlSegment { get; }
|
||||
|
||||
public int VersionId { get; }
|
||||
|
||||
public DateTime VersionDate { get; }
|
||||
|
||||
public int WriterId { get; }
|
||||
|
||||
public int? TemplateId { get; }
|
||||
|
||||
public bool Published { get; }
|
||||
|
||||
public Dictionary<string, PropertyData[]> Properties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The collection of language Id to name for the content item
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, CultureVariation>? CultureInfos { get; }
|
||||
}
|
||||
61
src/Umbraco.PublishedCache.HybridCache/ContentNode.cs
Normal file
61
src/Umbraco.PublishedCache.HybridCache/ContentNode.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
// represents a content "node" ie a pair of draft + published versions
|
||||
// internal, never exposed, to be accessed from ContentStore (only!)
|
||||
internal sealed class ContentNode
|
||||
{
|
||||
// everything that is common to both draft and published versions
|
||||
// keep this as small as possible
|
||||
#pragma warning disable IDE1006 // Naming Styles
|
||||
public readonly int Id;
|
||||
|
||||
|
||||
// draft and published version (either can be null, but not both)
|
||||
// are models not direct PublishedContent instances
|
||||
private ContentData? _draftData;
|
||||
private ContentData? _publishedData;
|
||||
|
||||
public ContentNode(
|
||||
int id,
|
||||
Guid key,
|
||||
int sortOrder,
|
||||
DateTime createDate,
|
||||
int creatorId,
|
||||
IPublishedContentType contentType,
|
||||
ContentData? draftData,
|
||||
ContentData? publishedData)
|
||||
{
|
||||
Id = id;
|
||||
Key = key;
|
||||
SortOrder = sortOrder;
|
||||
CreateDate = createDate;
|
||||
CreatorId = creatorId;
|
||||
ContentType = contentType;
|
||||
|
||||
if (draftData == null && publishedData == null)
|
||||
{
|
||||
throw new ArgumentException("Both draftData and publishedData cannot be null at the same time.");
|
||||
}
|
||||
|
||||
_draftData = draftData;
|
||||
_publishedData = publishedData;
|
||||
}
|
||||
|
||||
public bool HasPublished => _publishedData != null;
|
||||
|
||||
public ContentData? DraftModel => _draftData;
|
||||
|
||||
public ContentData? PublishedModel => _publishedData;
|
||||
|
||||
public readonly Guid Key;
|
||||
public IPublishedContentType ContentType;
|
||||
public readonly int SortOrder;
|
||||
public readonly DateTime CreateDate;
|
||||
public readonly int CreatorId;
|
||||
|
||||
public bool HasPublishedCulture(string culture) => _publishedData != null && (_publishedData.CultureInfos?.ContainsKey(culture) ?? false);
|
||||
#pragma warning restore IDE1006 // Naming Styles
|
||||
}
|
||||
29
src/Umbraco.PublishedCache.HybridCache/CultureVariation.cs
Normal file
29
src/Umbraco.PublishedCache.HybridCache/CultureVariation.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the culture variation information on a content item
|
||||
/// </summary>
|
||||
[DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys
|
||||
public class CultureVariation
|
||||
{
|
||||
[DataMember(Order = 0)]
|
||||
[JsonPropertyName("nm")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[DataMember(Order = 1)]
|
||||
[JsonPropertyName("us")]
|
||||
public string? UrlSegment { get; set; }
|
||||
|
||||
[DataMember(Order = 2)]
|
||||
[JsonPropertyName("dt")]
|
||||
[JsonConverter(typeof(JsonUniversalDateTimeConverter))]
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
[DataMember(Order = 3)]
|
||||
[JsonPropertyName("isd")]
|
||||
public bool IsDraft { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IUmbracoBuilder" /> for the Umbraco's NuCache
|
||||
/// </summary>
|
||||
public static class UmbracoBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Umbraco NuCache dependencies
|
||||
/// </summary>
|
||||
public static IUmbracoBuilder AddUmbracoHybridCache(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddHybridCache();
|
||||
builder.Services.AddSingleton<IDatabaseCacheRepository, DatabaseCacheRepository>();
|
||||
builder.Services.AddSingleton<IPublishedContentCache, DocumentCache>();
|
||||
builder.Services.AddSingleton<IPublishedMediaCache, MediaCache>();
|
||||
builder.Services.AddSingleton<IPublishedMemberCache, MemberCache>();
|
||||
builder.Services.AddSingleton<IDomainCache, DomainCache>();
|
||||
builder.Services.AddSingleton<IElementsCache, ElementsDictionaryAppCache>();
|
||||
builder.Services.AddSingleton<IPublishedContentTypeCache, PublishedContentTypeCache>();
|
||||
builder.Services.AddSingleton<IDocumentCacheService, DocumentCacheService>();
|
||||
builder.Services.AddSingleton<IMediaCacheService, MediaCacheService>();
|
||||
builder.Services.AddSingleton<IMemberCacheService, MemberCacheService>();
|
||||
builder.Services.AddSingleton<IDomainCacheService, DomainCacheService>();
|
||||
builder.Services.AddSingleton<IPublishedContentFactory, PublishedContentFactory>();
|
||||
builder.Services.AddSingleton<ICacheNodeFactory, CacheNodeFactory>();
|
||||
builder.Services.AddSingleton<ICacheManager, CacheManager>();
|
||||
builder.Services.AddSingleton<IContentCacheDataSerializerFactory>(s =>
|
||||
{
|
||||
IOptions<NuCacheSettings> options = s.GetRequiredService<IOptions<NuCacheSettings>>();
|
||||
switch (options.Value.NuCacheSerializerType)
|
||||
{
|
||||
case NuCacheSerializerType.JSON:
|
||||
return new JsonContentNestedDataSerializerFactory();
|
||||
case NuCacheSerializerType.MessagePack:
|
||||
return ActivatorUtilities.CreateInstance<MsgPackContentNestedDataSerializerFactory>(s);
|
||||
default:
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
});
|
||||
builder.Services.AddSingleton<IPropertyCacheCompressionOptions, NoopPropertyCacheCompressionOptions>();
|
||||
builder.AddNotificationAsyncHandler<ContentRefreshNotification, CacheRefreshingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<ContentDeletedNotification, CacheRefreshingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<MediaRefreshNotification, CacheRefreshingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<MediaDeletedNotification, CacheRefreshingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<UmbracoApplicationStartedNotification, SeedingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<ContentTypeRefreshedNotification, CacheRefreshingNotificationHandler>();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
64
src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs
Normal file
64
src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
public sealed class DocumentCache : IPublishedContentCache
|
||||
{
|
||||
private readonly IDocumentCacheService _documentCacheService;
|
||||
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
|
||||
|
||||
public DocumentCache(IDocumentCacheService documentCacheService, IPublishedContentTypeCache publishedContentTypeCache)
|
||||
{
|
||||
_documentCacheService = documentCacheService;
|
||||
_publishedContentTypeCache = publishedContentTypeCache;
|
||||
}
|
||||
|
||||
public async Task<IPublishedContent?> GetByIdAsync(int id, bool preview = false) => await _documentCacheService.GetByIdAsync(id, preview);
|
||||
|
||||
|
||||
public async Task<IPublishedContent?> GetByIdAsync(Guid key, bool preview = false) => await _documentCacheService.GetByKeyAsync(key, preview);
|
||||
|
||||
public IPublishedContent? GetById(bool preview, int contentId) => GetByIdAsync(contentId, preview).GetAwaiter().GetResult();
|
||||
|
||||
public IPublishedContent? GetById(bool preview, Guid contentId) => GetByIdAsync(contentId, preview).GetAwaiter().GetResult();
|
||||
|
||||
|
||||
public IPublishedContent? GetById(int contentId) => GetByIdAsync(contentId, false).GetAwaiter().GetResult();
|
||||
|
||||
public IPublishedContent? GetById(Guid contentId) => GetByIdAsync(contentId, false).GetAwaiter().GetResult();
|
||||
|
||||
public IPublishedContentType? GetContentType(int id) => _publishedContentTypeCache.Get(PublishedItemType.Content, id);
|
||||
|
||||
public IPublishedContentType? GetContentType(string alias) => _publishedContentTypeCache.Get(PublishedItemType.Content, alias);
|
||||
|
||||
|
||||
public IPublishedContentType? GetContentType(Guid key) => _publishedContentTypeCache.Get(PublishedItemType.Content, key);
|
||||
|
||||
// FIXME: These need to be refactored when removing nucache
|
||||
// Thats the time where we can change the IPublishedContentCache interface.
|
||||
|
||||
public IPublishedContent? GetById(bool preview, Udi contentId) => throw new NotImplementedException();
|
||||
|
||||
public IPublishedContent? GetById(Udi contentId) => throw new NotImplementedException();
|
||||
|
||||
public IEnumerable<IPublishedContent> GetAtRoot(bool preview, string? culture = null) => throw new NotImplementedException();
|
||||
|
||||
public IEnumerable<IPublishedContent> GetAtRoot(string? culture = null) => throw new NotImplementedException();
|
||||
|
||||
public bool HasContent(bool preview) => throw new NotImplementedException();
|
||||
|
||||
public bool HasContent() => throw new NotImplementedException();
|
||||
|
||||
public IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType) => throw new NotImplementedException();
|
||||
|
||||
public IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException();
|
||||
|
||||
public IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException();
|
||||
|
||||
public string? GetRouteById(bool preview, int contentId, string? culture = null) => throw new NotImplementedException();
|
||||
|
||||
public string? GetRouteById(int contentId, string? culture = null) => throw new NotImplementedException();
|
||||
}
|
||||
34
src/Umbraco.PublishedCache.HybridCache/DomainCache.cs
Normal file
34
src/Umbraco.PublishedCache.HybridCache/DomainCache.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="IDomainCache" /> for NuCache.
|
||||
/// </summary>
|
||||
public class DomainCache : IDomainCache
|
||||
{
|
||||
private readonly IDomainCacheService _domainCacheService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DomainCache" /> class.
|
||||
/// </summary>
|
||||
public DomainCache(IDefaultCultureAccessor defaultCultureAccessor, IDomainCacheService domainCacheService)
|
||||
{
|
||||
_domainCacheService = domainCacheService;
|
||||
DefaultCulture = defaultCultureAccessor.DefaultCulture;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DefaultCulture { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Domain> GetAll(bool includeWildcards) => _domainCacheService.GetAll(includeWildcards);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Domain> GetAssigned(int documentId, bool includeWildcards = false) => _domainCacheService.GetAssigned(documentId, includeWildcards);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasAssigned(int documentId, bool includeWildcards = false) => _domainCacheService.HasAssigned(documentId, includeWildcards);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
public class ElementsDictionaryAppCache : FastDictionaryAppCache, IElementsCache
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Factories;
|
||||
|
||||
internal class CacheNodeFactory : ICacheNodeFactory
|
||||
{
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly UrlSegmentProviderCollection _urlSegmentProviders;
|
||||
|
||||
public CacheNodeFactory(IShortStringHelper shortStringHelper, UrlSegmentProviderCollection urlSegmentProviders)
|
||||
{
|
||||
_shortStringHelper = shortStringHelper;
|
||||
_urlSegmentProviders = urlSegmentProviders;
|
||||
}
|
||||
|
||||
public ContentCacheNode ToContentCacheNode(IContent content, bool preview)
|
||||
{
|
||||
ContentData contentData = GetContentData(content, !preview, preview ? content.PublishTemplateId : content.TemplateId);
|
||||
return new ContentCacheNode
|
||||
{
|
||||
Id = content.Id,
|
||||
Key = content.Key,
|
||||
SortOrder = content.SortOrder,
|
||||
CreateDate = content.CreateDate,
|
||||
CreatorId = content.CreatorId,
|
||||
ContentTypeId = content.ContentTypeId,
|
||||
Data = contentData,
|
||||
IsDraft = preview,
|
||||
};
|
||||
}
|
||||
|
||||
public ContentCacheNode ToContentCacheNode(IMedia media)
|
||||
{
|
||||
ContentData contentData = GetContentData(media, false, null);
|
||||
return new ContentCacheNode
|
||||
{
|
||||
Id = media.Id,
|
||||
Key = media.Key,
|
||||
SortOrder = media.SortOrder,
|
||||
CreateDate = media.CreateDate,
|
||||
CreatorId = media.CreatorId,
|
||||
ContentTypeId = media.ContentTypeId,
|
||||
Data = contentData,
|
||||
IsDraft = false,
|
||||
};
|
||||
}
|
||||
|
||||
private ContentData GetContentData(IContentBase content, bool published, int? templateId)
|
||||
{
|
||||
var propertyData = new Dictionary<string, PropertyData[]>();
|
||||
foreach (IProperty prop in content.Properties)
|
||||
{
|
||||
var pdatas = new List<PropertyData>();
|
||||
foreach (IPropertyValue pvalue in prop.Values.OrderBy(x => x.Culture))
|
||||
{
|
||||
// sanitize - properties should be ok but ... never knows
|
||||
if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// note: at service level, invariant is 'null', but here invariant becomes 'string.Empty'
|
||||
var value = published ? pvalue.PublishedValue : pvalue.EditedValue;
|
||||
if (value != null)
|
||||
{
|
||||
pdatas.Add(new PropertyData
|
||||
{
|
||||
Culture = pvalue.Culture ?? string.Empty,
|
||||
Segment = pvalue.Segment ?? string.Empty,
|
||||
Value = value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
propertyData[prop.Alias] = pdatas.ToArray();
|
||||
}
|
||||
|
||||
var cultureData = new Dictionary<string, CultureVariation>();
|
||||
|
||||
// sanitize - names should be ok but ... never knows
|
||||
if (content.ContentType.VariesByCulture())
|
||||
{
|
||||
ContentCultureInfosCollection? infos = content is IContent document
|
||||
? published
|
||||
? document.PublishCultureInfos
|
||||
: document.CultureInfos
|
||||
: content.CultureInfos;
|
||||
|
||||
// ReSharper disable once UseDeconstruction
|
||||
if (infos is not null)
|
||||
{
|
||||
foreach (ContentCultureInfos cultureInfo in infos)
|
||||
{
|
||||
var cultureIsDraft = !published && content is IContent d && d.IsCultureEdited(cultureInfo.Culture);
|
||||
cultureData[cultureInfo.Culture] = new CultureVariation
|
||||
{
|
||||
Name = cultureInfo.Name,
|
||||
UrlSegment =
|
||||
content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, cultureInfo.Culture),
|
||||
Date = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue,
|
||||
IsDraft = cultureIsDraft,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ContentData(
|
||||
content.Name,
|
||||
null,
|
||||
content.VersionId,
|
||||
content.UpdateDate,
|
||||
content.CreatorId,
|
||||
templateId,
|
||||
published,
|
||||
propertyData,
|
||||
cultureData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Factories;
|
||||
|
||||
internal interface ICacheNodeFactory
|
||||
{
|
||||
ContentCacheNode ToContentCacheNode(IContent content, bool preview);
|
||||
ContentCacheNode ToContentCacheNode(IMedia media);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Factories;
|
||||
|
||||
internal interface IPublishedContentFactory
|
||||
{
|
||||
IPublishedContent? ToIPublishedContent(ContentCacheNode contentCacheNode, bool preview);
|
||||
IPublishedContent? ToIPublishedMedia(ContentCacheNode contentCacheNode);
|
||||
|
||||
IPublishedMember ToPublishedMember(IMember member);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Factories;
|
||||
|
||||
internal class PublishedContentFactory : IPublishedContentFactory
|
||||
{
|
||||
private readonly IElementsCache _elementsCache;
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
|
||||
|
||||
|
||||
public PublishedContentFactory(
|
||||
IElementsCache elementsCache,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IPublishedContentTypeCache publishedContentTypeCache)
|
||||
{
|
||||
_elementsCache = elementsCache;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_publishedContentTypeCache = publishedContentTypeCache;
|
||||
}
|
||||
|
||||
public IPublishedContent? ToIPublishedContent(ContentCacheNode contentCacheNode, bool preview)
|
||||
{
|
||||
IPublishedContentType contentType = _publishedContentTypeCache.Get(PublishedItemType.Content, contentCacheNode.ContentTypeId);
|
||||
var contentNode = new ContentNode(
|
||||
contentCacheNode.Id,
|
||||
contentCacheNode.Key,
|
||||
contentCacheNode.SortOrder,
|
||||
contentCacheNode.CreateDate,
|
||||
contentCacheNode.CreatorId,
|
||||
contentType,
|
||||
preview ? contentCacheNode.Data : null,
|
||||
preview ? null : contentCacheNode.Data);
|
||||
|
||||
IPublishedContent? model = GetModel(contentNode, preview);
|
||||
|
||||
if (preview)
|
||||
{
|
||||
return model ?? GetPublishedContentAsDraft(model);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
public IPublishedContent? ToIPublishedMedia(ContentCacheNode contentCacheNode)
|
||||
{
|
||||
IPublishedContentType contentType = _publishedContentTypeCache.Get(PublishedItemType.Media, contentCacheNode.ContentTypeId);
|
||||
var contentNode = new ContentNode(
|
||||
contentCacheNode.Id,
|
||||
contentCacheNode.Key,
|
||||
contentCacheNode.SortOrder,
|
||||
contentCacheNode.CreateDate,
|
||||
contentCacheNode.CreatorId,
|
||||
contentType,
|
||||
null,
|
||||
contentCacheNode.Data);
|
||||
|
||||
return GetModel(contentNode, false);
|
||||
}
|
||||
|
||||
public IPublishedMember ToPublishedMember(IMember member)
|
||||
{
|
||||
IPublishedContentType contentType = _publishedContentTypeCache.Get(PublishedItemType.Member, member.ContentTypeId);
|
||||
|
||||
// Members are only "mapped" never cached, so these default values are a bit wierd, but they are not used.
|
||||
var contentData = new ContentData(
|
||||
member.Name,
|
||||
null,
|
||||
0,
|
||||
member.UpdateDate,
|
||||
member.CreatorId,
|
||||
null,
|
||||
true,
|
||||
GetPropertyValues(contentType, member),
|
||||
null);
|
||||
|
||||
var contentNode = new ContentNode(
|
||||
member.Id,
|
||||
member.Key,
|
||||
member.SortOrder,
|
||||
member.UpdateDate,
|
||||
member.CreatorId,
|
||||
contentType,
|
||||
null,
|
||||
contentData);
|
||||
return new PublishedMember(member, contentNode, _elementsCache, _variationContextAccessor);
|
||||
}
|
||||
|
||||
private Dictionary<string, PropertyData[]> GetPropertyValues(IPublishedContentType contentType, IMember member)
|
||||
{
|
||||
var properties = member
|
||||
.Properties
|
||||
.ToDictionary(
|
||||
x => x.Alias,
|
||||
x => new[] { new PropertyData { Value = x.GetValue(), Culture = string.Empty, Segment = string.Empty } },
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add member properties
|
||||
AddIf(contentType, properties, nameof(IMember.Email), member.Email);
|
||||
AddIf(contentType, properties, nameof(IMember.Username), member.Username);
|
||||
AddIf(contentType, properties, nameof(IMember.Comments), member.Comments);
|
||||
AddIf(contentType, properties, nameof(IMember.IsApproved), member.IsApproved);
|
||||
AddIf(contentType, properties, nameof(IMember.IsLockedOut), member.IsLockedOut);
|
||||
AddIf(contentType, properties, nameof(IMember.LastLockoutDate), member.LastLockoutDate);
|
||||
AddIf(contentType, properties, nameof(IMember.CreateDate), member.CreateDate);
|
||||
AddIf(contentType, properties, nameof(IMember.LastLoginDate), member.LastLoginDate);
|
||||
AddIf(contentType, properties, nameof(IMember.LastPasswordChangeDate), member.LastPasswordChangeDate);
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private void AddIf(IPublishedContentType contentType, IDictionary<string, PropertyData[]> properties, string alias, object? value)
|
||||
{
|
||||
IPublishedPropertyType? propertyType = contentType.GetPropertyType(alias);
|
||||
if (propertyType == null || propertyType.IsUserProperty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
properties[alias] = new[] { new PropertyData { Value = value, Culture = string.Empty, Segment = string.Empty } };
|
||||
}
|
||||
|
||||
private IPublishedContent? GetModel(ContentNode node, bool preview)
|
||||
{
|
||||
ContentData? contentData = preview ? node.DraftModel : node.PublishedModel;
|
||||
return contentData == null
|
||||
? null
|
||||
: new PublishedContent(
|
||||
node,
|
||||
preview,
|
||||
_elementsCache,
|
||||
_variationContextAccessor);
|
||||
}
|
||||
|
||||
|
||||
private IPublishedContent? GetPublishedContentAsDraft(IPublishedContent? content) =>
|
||||
content == null ? null :
|
||||
// an object in the cache is either an IPublishedContentOrMedia,
|
||||
// or a model inheriting from PublishedContentExtended - in which
|
||||
// case we need to unwrap to get to the original IPublishedContentOrMedia.
|
||||
UnwrapIPublishedContent(content);
|
||||
|
||||
private PublishedContent UnwrapIPublishedContent(IPublishedContent content)
|
||||
{
|
||||
while (content is PublishedContentWrapped wrapped)
|
||||
{
|
||||
content = wrapped.Unwrap();
|
||||
}
|
||||
|
||||
if (!(content is PublishedContent inner))
|
||||
{
|
||||
throw new InvalidOperationException("Innermost content is not PublishedContent.");
|
||||
}
|
||||
|
||||
return inner;
|
||||
}
|
||||
}
|
||||
7
src/Umbraco.PublishedCache.HybridCache/IElementsCache.cs
Normal file
7
src/Umbraco.PublishedCache.HybridCache/IElementsCache.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
public interface IElementsCache : IAppCache
|
||||
{
|
||||
}
|
||||
56
src/Umbraco.PublishedCache.HybridCache/MediaCache.cs
Normal file
56
src/Umbraco.PublishedCache.HybridCache/MediaCache.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
public class MediaCache : IPublishedMediaCache
|
||||
{
|
||||
private readonly IMediaCacheService _mediaCacheService;
|
||||
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
|
||||
|
||||
public MediaCache(IMediaCacheService mediaCacheService, IPublishedContentTypeCache publishedContentTypeCache)
|
||||
{
|
||||
_mediaCacheService = mediaCacheService;
|
||||
_publishedContentTypeCache = publishedContentTypeCache;
|
||||
}
|
||||
|
||||
public async Task<IPublishedContent?> GetByIdAsync(int id) => await _mediaCacheService.GetByIdAsync(id);
|
||||
|
||||
public async Task<IPublishedContent?> GetByKeyAsync(Guid key) => await _mediaCacheService.GetByKeyAsync(key);
|
||||
|
||||
public IPublishedContent? GetById(bool preview, int contentId) => GetByIdAsync(contentId).GetAwaiter().GetResult();
|
||||
|
||||
public IPublishedContent? GetById(bool preview, Guid contentId) =>
|
||||
GetByKeyAsync(contentId).GetAwaiter().GetResult();
|
||||
|
||||
|
||||
public IPublishedContent? GetById(int contentId) => GetByIdAsync(contentId).GetAwaiter().GetResult();
|
||||
|
||||
public IPublishedContent? GetById(Guid contentId) => GetByKeyAsync(contentId).GetAwaiter().GetResult();
|
||||
|
||||
|
||||
public IPublishedContentType? GetContentType(Guid key) => _publishedContentTypeCache.Get(PublishedItemType.Media, key);
|
||||
|
||||
public IPublishedContentType GetContentType(int id) => _publishedContentTypeCache.Get(PublishedItemType.Media, id);
|
||||
|
||||
public IPublishedContentType GetContentType(string alias) => _publishedContentTypeCache.Get(PublishedItemType.Media, alias);
|
||||
|
||||
// FIXME - these need to be removed when removing nucache
|
||||
public IPublishedContent? GetById(bool preview, Udi contentId) => throw new NotImplementedException();
|
||||
|
||||
public IPublishedContent? GetById(Udi contentId) => throw new NotImplementedException();
|
||||
|
||||
public IEnumerable<IPublishedContent> GetAtRoot(bool preview, string? culture = null) => throw new NotImplementedException();
|
||||
|
||||
public IEnumerable<IPublishedContent> GetAtRoot(string? culture = null) => throw new NotImplementedException();
|
||||
|
||||
public bool HasContent(bool preview) => throw new NotImplementedException();
|
||||
|
||||
public bool HasContent() => throw new NotImplementedException();
|
||||
|
||||
|
||||
public IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType) =>
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
27
src/Umbraco.PublishedCache.HybridCache/MemberCache.cs
Normal file
27
src/Umbraco.PublishedCache.HybridCache/MemberCache.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
public class MemberCache : IPublishedMemberCache
|
||||
{
|
||||
private readonly IMemberCacheService _memberCacheService;
|
||||
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
|
||||
|
||||
public MemberCache(IMemberCacheService memberCacheService, IPublishedContentTypeCache publishedContentTypeCache)
|
||||
{
|
||||
_memberCacheService = memberCacheService;
|
||||
_publishedContentTypeCache = publishedContentTypeCache;
|
||||
}
|
||||
|
||||
public async Task<IPublishedMember?> GetAsync(IMember member) =>
|
||||
await _memberCacheService.Get(member);
|
||||
|
||||
public IPublishedMember? Get(IMember member) => GetAsync(member).GetAwaiter().GetResult();
|
||||
|
||||
public IPublishedContentType GetContentType(int id) => _publishedContentTypeCache.Get(PublishedItemType.Member, id);
|
||||
|
||||
public IPublishedContentType GetContentType(string alias) => _publishedContentTypeCache.Get(PublishedItemType.Member, alias);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers;
|
||||
|
||||
internal sealed class CacheRefreshingNotificationHandler :
|
||||
INotificationAsyncHandler<ContentRefreshNotification>,
|
||||
INotificationAsyncHandler<ContentDeletedNotification>,
|
||||
INotificationAsyncHandler<MediaRefreshNotification>,
|
||||
INotificationAsyncHandler<MediaDeletedNotification>,
|
||||
INotificationAsyncHandler<ContentTypeRefreshedNotification>
|
||||
{
|
||||
private readonly IDocumentCacheService _documentCacheService;
|
||||
private readonly IMediaCacheService _mediaCacheService;
|
||||
private readonly IElementsCache _elementsCache;
|
||||
private readonly IRelationService _relationService;
|
||||
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
|
||||
|
||||
public CacheRefreshingNotificationHandler(
|
||||
IDocumentCacheService documentCacheService,
|
||||
IMediaCacheService mediaCacheService,
|
||||
IElementsCache elementsCache,
|
||||
IRelationService relationService,
|
||||
IPublishedContentTypeCache publishedContentTypeCache)
|
||||
{
|
||||
_documentCacheService = documentCacheService;
|
||||
_mediaCacheService = mediaCacheService;
|
||||
_elementsCache = elementsCache;
|
||||
_relationService = relationService;
|
||||
_publishedContentTypeCache = publishedContentTypeCache;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(ContentRefreshNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await RefreshElementsCacheAsync(notification.Entity);
|
||||
|
||||
await _documentCacheService.RefreshContentAsync(notification.Entity);
|
||||
}
|
||||
|
||||
public async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (IContent deletedEntity in notification.DeletedEntities)
|
||||
{
|
||||
await RefreshElementsCacheAsync(deletedEntity);
|
||||
await _documentCacheService.DeleteItemAsync(deletedEntity.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleAsync(MediaRefreshNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await RefreshElementsCacheAsync(notification.Entity);
|
||||
await _mediaCacheService.RefreshMediaAsync(notification.Entity);
|
||||
}
|
||||
|
||||
public async Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (IMedia deletedEntity in notification.DeletedEntities)
|
||||
{
|
||||
await RefreshElementsCacheAsync(deletedEntity);
|
||||
await _mediaCacheService.DeleteItemAsync(deletedEntity.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshElementsCacheAsync(IUmbracoEntity content)
|
||||
{
|
||||
IEnumerable<IRelation> parentRelations = _relationService.GetByParent(content)!;
|
||||
IEnumerable<IRelation> childRelations = _relationService.GetByChild(content);
|
||||
|
||||
var ids = parentRelations.Select(x => x.ChildId).Concat(childRelations.Select(x => x.ParentId)).ToHashSet();
|
||||
foreach (var id in ids)
|
||||
{
|
||||
if (await _documentCacheService.HasContentByIdAsync(id) is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IPublishedContent? publishedContent = await _documentCacheService.GetByIdAsync(id);
|
||||
if (publishedContent is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (IPublishedProperty publishedProperty in publishedContent.Properties)
|
||||
{
|
||||
var property = (PublishedProperty) publishedProperty;
|
||||
if (property.ReferenceCacheLevel != PropertyCacheLevel.Elements)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_elementsCache.ClearByKey(property.ValuesCacheKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task HandleAsync(ContentTypeRefreshedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
const ContentTypeChangeTypes types // only for those that have been refreshed
|
||||
= ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther | ContentTypeChangeTypes.Remove;
|
||||
var contentTypeIds = notification.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id)
|
||||
.ToArray();
|
||||
|
||||
if (contentTypeIds.Length != 0)
|
||||
{
|
||||
foreach (var contentTypeId in contentTypeIds)
|
||||
{
|
||||
_publishedContentTypeCache.ClearContentType(contentTypeId);
|
||||
}
|
||||
|
||||
_documentCacheService.Rebuild(contentTypeIds);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers;
|
||||
|
||||
internal class SeedingNotificationHandler : INotificationAsyncHandler<UmbracoApplicationStartedNotification>
|
||||
{
|
||||
private readonly IDocumentCacheService _documentCacheService;
|
||||
private readonly CacheSettings _cacheSettings;
|
||||
|
||||
public SeedingNotificationHandler(IDocumentCacheService documentCacheService, IOptions<CacheSettings> cacheSettings)
|
||||
{
|
||||
_documentCacheService = documentCacheService;
|
||||
_cacheSettings = cacheSettings.Value;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(UmbracoApplicationStartedNotification notification, CancellationToken cancellationToken) => await _documentCacheService.SeedAsync(_cacheSettings.ContentTypeKeys);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Persistence
|
||||
{
|
||||
// read-only dto
|
||||
internal class ContentSourceDto : IReadOnlyContentBase
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public Guid Key { get; init; }
|
||||
|
||||
public int ContentTypeId { get; init; }
|
||||
|
||||
public int Level { get; init; }
|
||||
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public int SortOrder { get; init; }
|
||||
|
||||
public int ParentId { get; init; }
|
||||
|
||||
public bool Published { get; init; }
|
||||
|
||||
public bool Edited { get; init; }
|
||||
|
||||
public DateTime CreateDate { get; init; }
|
||||
|
||||
public int CreatorId { get; init; }
|
||||
|
||||
// edited data
|
||||
public int VersionId { get; init; }
|
||||
|
||||
public string? EditName { get; init; }
|
||||
|
||||
public DateTime EditVersionDate { get; init; }
|
||||
|
||||
public int EditWriterId { get; init; }
|
||||
|
||||
public int EditTemplateId { get; init; }
|
||||
|
||||
public string? EditData { get; init; }
|
||||
|
||||
public byte[]? EditDataRaw { get; init; }
|
||||
|
||||
// published data
|
||||
public int PublishedVersionId { get; init; }
|
||||
|
||||
public string? PubName { get; init; }
|
||||
|
||||
public DateTime PubVersionDate { get; init; }
|
||||
|
||||
public int PubWriterId { get; init; }
|
||||
|
||||
public int PubTemplateId { get; init; }
|
||||
|
||||
public string? PubData { get; init; }
|
||||
|
||||
public byte[]? PubDataRaw { get; init; }
|
||||
|
||||
// Explicit implementation
|
||||
DateTime IReadOnlyContentBase.UpdateDate => EditVersionDate;
|
||||
|
||||
string? IReadOnlyContentBase.Name => EditName;
|
||||
|
||||
int IReadOnlyContentBase.WriterId => EditWriterId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,895 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax;
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
using Umbraco.Extensions;
|
||||
using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Persistence;
|
||||
|
||||
internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRepository
|
||||
{
|
||||
private readonly IContentCacheDataSerializerFactory _contentCacheDataSerializerFactory;
|
||||
private readonly IDocumentRepository _documentRepository;
|
||||
private readonly ILogger<DatabaseCacheRepository> _logger;
|
||||
private readonly IMediaRepository _mediaRepository;
|
||||
private readonly IMemberRepository _memberRepository;
|
||||
private readonly IOptions<NuCacheSettings> _nucacheSettings;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly UrlSegmentProviderCollection _urlSegmentProviders;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DatabaseCacheRepository" /> class.
|
||||
/// </summary>
|
||||
public DatabaseCacheRepository(
|
||||
IScopeAccessor scopeAccessor,
|
||||
AppCaches appCaches,
|
||||
ILogger<DatabaseCacheRepository> logger,
|
||||
IMemberRepository memberRepository,
|
||||
IDocumentRepository documentRepository,
|
||||
IMediaRepository mediaRepository,
|
||||
IShortStringHelper shortStringHelper,
|
||||
UrlSegmentProviderCollection urlSegmentProviders,
|
||||
IContentCacheDataSerializerFactory contentCacheDataSerializerFactory,
|
||||
IOptions<NuCacheSettings> nucacheSettings)
|
||||
: base(scopeAccessor, appCaches)
|
||||
{
|
||||
_logger = logger;
|
||||
_memberRepository = memberRepository;
|
||||
_documentRepository = documentRepository;
|
||||
_mediaRepository = mediaRepository;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
_urlSegmentProviders = urlSegmentProviders;
|
||||
_contentCacheDataSerializerFactory = contentCacheDataSerializerFactory;
|
||||
_nucacheSettings = nucacheSettings;
|
||||
}
|
||||
|
||||
public async Task DeleteContentItemAsync(int id)
|
||||
=> await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId=@id", new { id = id });
|
||||
|
||||
public async Task RefreshContentAsync(ContentCacheNode contentCacheNode, PublishedState publishedState)
|
||||
{
|
||||
IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
|
||||
|
||||
// always refresh the edited data
|
||||
await OnRepositoryRefreshed(serializer, contentCacheNode, true);
|
||||
|
||||
switch (publishedState)
|
||||
{
|
||||
case PublishedState.Publishing:
|
||||
await OnRepositoryRefreshed(serializer, contentCacheNode, false);
|
||||
break;
|
||||
case PublishedState.Unpublishing:
|
||||
await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId=@id AND published=1", new { id = contentCacheNode.Id });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshMediaAsync(ContentCacheNode contentCacheNode)
|
||||
{
|
||||
IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media);
|
||||
await OnRepositoryRefreshed(serializer, contentCacheNode, false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Rebuild(
|
||||
IReadOnlyCollection<int>? contentTypeIds = null,
|
||||
IReadOnlyCollection<int>? mediaTypeIds = null,
|
||||
IReadOnlyCollection<int>? memberTypeIds = null)
|
||||
{
|
||||
IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(
|
||||
ContentCacheDataSerializerEntityType.Document
|
||||
| ContentCacheDataSerializerEntityType.Media
|
||||
| ContentCacheDataSerializerEntityType.Member);
|
||||
|
||||
// If contentTypeIds, mediaTypeIds and memberTypeIds are null, truncate table as all records will be deleted (as these 3 are the only types in the table).
|
||||
if (contentTypeIds != null && !contentTypeIds.Any()
|
||||
&& mediaTypeIds != null && !mediaTypeIds.Any()
|
||||
&& memberTypeIds != null && !memberTypeIds.Any())
|
||||
{
|
||||
if (Database.DatabaseType == DatabaseType.SqlServer2012)
|
||||
{
|
||||
Database.Execute($"TRUNCATE TABLE cmsContentNu");
|
||||
}
|
||||
|
||||
if (Database.DatabaseType == DatabaseType.SQLite)
|
||||
{
|
||||
Database.Execute($"DELETE FROM cmsContentNu");
|
||||
}
|
||||
}
|
||||
|
||||
if (contentTypeIds != null)
|
||||
{
|
||||
RebuildContentDbCache(serializer, _nucacheSettings.Value.SqlPageSize, contentTypeIds);
|
||||
}
|
||||
|
||||
if (mediaTypeIds != null)
|
||||
{
|
||||
RebuildMediaDbCache(serializer, _nucacheSettings.Value.SqlPageSize, mediaTypeIds);
|
||||
}
|
||||
|
||||
if (memberTypeIds != null)
|
||||
{
|
||||
RebuildMemberDbCache(serializer, _nucacheSettings.Value.SqlPageSize, memberTypeIds);
|
||||
}
|
||||
}
|
||||
|
||||
// assumes content tree lock
|
||||
public bool VerifyContentDbCache()
|
||||
{
|
||||
// every document should have a corresponding row for edited properties
|
||||
// and if published, may have a corresponding row for published properties
|
||||
Guid contentObjectType = Constants.ObjectTypes.Document;
|
||||
|
||||
var count = Database.ExecuteScalar<int>(
|
||||
$@"SELECT COUNT(*)
|
||||
FROM umbracoNode
|
||||
JOIN {Constants.DatabaseSchema.Tables.Document} ON umbracoNode.id={Constants.DatabaseSchema.Tables.Document}.nodeId
|
||||
LEFT JOIN cmsContentNu nuEdited ON (umbracoNode.id=nuEdited.nodeId AND nuEdited.published=0)
|
||||
LEFT JOIN cmsContentNu nuPublished ON (umbracoNode.id=nuPublished.nodeId AND nuPublished.published=1)
|
||||
WHERE umbracoNode.nodeObjectType=@objType
|
||||
AND nuEdited.nodeId IS NULL OR ({Constants.DatabaseSchema.Tables.Document}.published=1 AND nuPublished.nodeId IS NULL);",
|
||||
new { objType = contentObjectType });
|
||||
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
// assumes media tree lock
|
||||
public bool VerifyMediaDbCache()
|
||||
{
|
||||
// every media item should have a corresponding row for edited properties
|
||||
Guid mediaObjectType = Constants.ObjectTypes.Media;
|
||||
|
||||
var count = Database.ExecuteScalar<int>(
|
||||
@"SELECT COUNT(*)
|
||||
FROM umbracoNode
|
||||
LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0)
|
||||
WHERE umbracoNode.nodeObjectType=@objType
|
||||
AND cmsContentNu.nodeId IS NULL
|
||||
",
|
||||
new { objType = mediaObjectType });
|
||||
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
// assumes member tree lock
|
||||
public bool VerifyMemberDbCache()
|
||||
{
|
||||
// every member item should have a corresponding row for edited properties
|
||||
Guid memberObjectType = Constants.ObjectTypes.Member;
|
||||
|
||||
var count = Database.ExecuteScalar<int>(
|
||||
@"SELECT COUNT(*)
|
||||
FROM umbracoNode
|
||||
LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0)
|
||||
WHERE umbracoNode.nodeObjectType=@objType
|
||||
AND cmsContentNu.nodeId IS NULL
|
||||
",
|
||||
new { objType = memberObjectType });
|
||||
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
public async Task<ContentCacheNode?> GetContentSourceAsync(int id, bool preview = false)
|
||||
{
|
||||
Sql<ISqlContext>? sql = SqlContentSourcesSelect()
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document))
|
||||
.Append(SqlWhereNodeId(SqlContext, id))
|
||||
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
|
||||
|
||||
ContentSourceDto? dto = await Database.FirstOrDefaultAsync<ContentSourceDto>(sql);
|
||||
|
||||
if (dto == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preview is false && dto.PubDataRaw is null && dto.PubData is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IContentCacheDataSerializer serializer =
|
||||
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
|
||||
return CreateContentNodeKit(dto, serializer, preview);
|
||||
}
|
||||
|
||||
public IEnumerable<ContentCacheNode> GetContentByContentTypeKey(IEnumerable<Guid> keys)
|
||||
{
|
||||
if (keys.Any() is false)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
Sql<ISqlContext>? sql = SqlContentSourcesSelect()
|
||||
.InnerJoin<NodeDto>("n")
|
||||
.On<NodeDto, ContentDto>((n, c) => n.NodeId == c.ContentTypeId, "n", "umbracoContent")
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document))
|
||||
.WhereIn<NodeDto>(x => x.UniqueId, keys,"n")
|
||||
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
|
||||
|
||||
IContentCacheDataSerializer serializer =
|
||||
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
|
||||
|
||||
IEnumerable<ContentSourceDto> dtos = GetContentNodeDtos(sql);
|
||||
|
||||
foreach (ContentSourceDto row in dtos)
|
||||
{
|
||||
yield return CreateContentNodeKit(row, serializer, row.Published is false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ContentCacheNode?> GetMediaSourceAsync(int id)
|
||||
{
|
||||
Sql<ISqlContext>? sql = SqlMediaSourcesSelect()
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media))
|
||||
.Append(SqlWhereNodeId(SqlContext, id))
|
||||
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
|
||||
|
||||
ContentSourceDto? dto = await Database.FirstOrDefaultAsync<ContentSourceDto>(sql);
|
||||
|
||||
if (dto is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IContentCacheDataSerializer serializer =
|
||||
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media);
|
||||
return CreateMediaNodeKit(dto, serializer);
|
||||
}
|
||||
|
||||
private async Task OnRepositoryRefreshed(IContentCacheDataSerializer serializer, ContentCacheNode content, bool preview)
|
||||
{
|
||||
// use a custom SQL to update row version on each update
|
||||
// db.InsertOrUpdate(dto);
|
||||
ContentNuDto dto = GetDtoFromCacheNode(content, !preview, serializer);
|
||||
|
||||
await Database.InsertOrUpdateAsync(
|
||||
dto,
|
||||
"SET data=@data, dataRaw=@dataRaw, rv=rv+1 WHERE nodeId=@id AND published=@published",
|
||||
new
|
||||
{
|
||||
dataRaw = dto.RawData ?? Array.Empty<byte>(),
|
||||
data = dto.Data,
|
||||
id = dto.NodeId,
|
||||
published = dto.Published,
|
||||
});
|
||||
}
|
||||
|
||||
// assumes content tree lock
|
||||
private void RebuildContentDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection<int>? contentTypeIds)
|
||||
{
|
||||
Guid contentObjectType = Constants.ObjectTypes.Document;
|
||||
|
||||
// remove all - if anything fails the transaction will rollback
|
||||
if (contentTypeIds == null || contentTypeIds.Count == 0)
|
||||
{
|
||||
// must support SQL-CE
|
||||
Database.Execute(
|
||||
@"DELETE FROM cmsContentNu
|
||||
WHERE cmsContentNu.nodeId IN (
|
||||
SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType
|
||||
)",
|
||||
new { objType = contentObjectType });
|
||||
}
|
||||
else
|
||||
{
|
||||
// assume number of ctypes won't blow IN(...)
|
||||
// must support SQL-CE
|
||||
Database.Execute(
|
||||
$@"DELETE FROM cmsContentNu
|
||||
WHERE cmsContentNu.nodeId IN (
|
||||
SELECT id FROM umbracoNode
|
||||
JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id
|
||||
WHERE umbracoNode.nodeObjectType=@objType
|
||||
AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes)
|
||||
)",
|
||||
new { objType = contentObjectType, ctypes = contentTypeIds });
|
||||
}
|
||||
|
||||
// insert back - if anything fails the transaction will rollback
|
||||
IQuery<IContent> query = SqlContext.Query<IContent>();
|
||||
if (contentTypeIds != null && contentTypeIds.Count > 0)
|
||||
{
|
||||
query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...)
|
||||
}
|
||||
|
||||
long pageIndex = 0;
|
||||
long processed = 0;
|
||||
long total;
|
||||
do
|
||||
{
|
||||
// the tree is locked, counting and comparing to total is safe
|
||||
IEnumerable<IContent> descendants =
|
||||
_documentRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path"));
|
||||
var items = new List<ContentNuDto>();
|
||||
var count = 0;
|
||||
foreach (IContent c in descendants)
|
||||
{
|
||||
// always the edited version
|
||||
items.Add(GetDtoFromContent(c, false, serializer));
|
||||
|
||||
// and also the published version if it makes any sense
|
||||
if (c.Published)
|
||||
{
|
||||
items.Add(GetDtoFromContent(c, true, serializer));
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
Database.BulkInsertRecords(items);
|
||||
processed += count;
|
||||
} while (processed < total);
|
||||
}
|
||||
|
||||
// assumes media tree lock
|
||||
private void RebuildMediaDbCache(IContentCacheDataSerializer serializer, int groupSize,
|
||||
IReadOnlyCollection<int>? contentTypeIds)
|
||||
{
|
||||
Guid mediaObjectType = Constants.ObjectTypes.Media;
|
||||
|
||||
// remove all - if anything fails the transaction will rollback
|
||||
if (contentTypeIds is null || contentTypeIds.Count == 0)
|
||||
{
|
||||
// must support SQL-CE
|
||||
Database.Execute(
|
||||
@"DELETE FROM cmsContentNu
|
||||
WHERE cmsContentNu.nodeId IN (
|
||||
SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType
|
||||
)",
|
||||
new { objType = mediaObjectType });
|
||||
}
|
||||
else
|
||||
{
|
||||
// assume number of ctypes won't blow IN(...)
|
||||
// must support SQL-CE
|
||||
Database.Execute(
|
||||
$@"DELETE FROM cmsContentNu
|
||||
WHERE cmsContentNu.nodeId IN (
|
||||
SELECT id FROM umbracoNode
|
||||
JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id
|
||||
WHERE umbracoNode.nodeObjectType=@objType
|
||||
AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes)
|
||||
)",
|
||||
new { objType = mediaObjectType, ctypes = contentTypeIds });
|
||||
}
|
||||
|
||||
// insert back - if anything fails the transaction will rollback
|
||||
IQuery<IMedia> query = SqlContext.Query<IMedia>();
|
||||
if (contentTypeIds is not null && contentTypeIds.Count > 0)
|
||||
{
|
||||
query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...)
|
||||
}
|
||||
|
||||
long pageIndex = 0;
|
||||
long processed = 0;
|
||||
long total;
|
||||
do
|
||||
{
|
||||
// the tree is locked, counting and comparing to total is safe
|
||||
IEnumerable<IMedia> descendants =
|
||||
_mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path"));
|
||||
var items = descendants.Select(m => GetDtoFromContent(m, false, serializer)).ToArray();
|
||||
Database.BulkInsertRecords(items);
|
||||
processed += items.Length;
|
||||
} while (processed < total);
|
||||
}
|
||||
|
||||
// assumes member tree lock
|
||||
private void RebuildMemberDbCache(IContentCacheDataSerializer serializer, int groupSize,
|
||||
IReadOnlyCollection<int>? contentTypeIds)
|
||||
{
|
||||
Guid memberObjectType = Constants.ObjectTypes.Member;
|
||||
|
||||
// remove all - if anything fails the transaction will rollback
|
||||
if (contentTypeIds == null || contentTypeIds.Count == 0)
|
||||
{
|
||||
// must support SQL-CE
|
||||
Database.Execute(
|
||||
@"DELETE FROM cmsContentNu
|
||||
WHERE cmsContentNu.nodeId IN (
|
||||
SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType
|
||||
)",
|
||||
new { objType = memberObjectType });
|
||||
}
|
||||
else
|
||||
{
|
||||
// assume number of ctypes won't blow IN(...)
|
||||
// must support SQL-CE
|
||||
Database.Execute(
|
||||
$@"DELETE FROM cmsContentNu
|
||||
WHERE cmsContentNu.nodeId IN (
|
||||
SELECT id FROM umbracoNode
|
||||
JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id
|
||||
WHERE umbracoNode.nodeObjectType=@objType
|
||||
AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes)
|
||||
)",
|
||||
new { objType = memberObjectType, ctypes = contentTypeIds });
|
||||
}
|
||||
|
||||
// insert back - if anything fails the transaction will rollback
|
||||
IQuery<IMember> query = SqlContext.Query<IMember>();
|
||||
if (contentTypeIds != null && contentTypeIds.Count > 0)
|
||||
{
|
||||
query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...)
|
||||
}
|
||||
|
||||
long pageIndex = 0;
|
||||
long processed = 0;
|
||||
long total;
|
||||
do
|
||||
{
|
||||
IEnumerable<IMember> descendants =
|
||||
_memberRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path"));
|
||||
ContentNuDto[] items = descendants.Select(m => GetDtoFromContent(m, false, serializer)).ToArray();
|
||||
Database.BulkInsertRecords(items);
|
||||
processed += items.Length;
|
||||
} while (processed < total);
|
||||
}
|
||||
|
||||
private ContentNuDto GetDtoFromCacheNode(ContentCacheNode cacheNode, bool published, IContentCacheDataSerializer serializer)
|
||||
{
|
||||
// the dictionary that will be serialized
|
||||
var contentCacheData = new ContentCacheDataModel
|
||||
{
|
||||
PropertyData = cacheNode.Data?.Properties,
|
||||
CultureData = cacheNode.Data?.CultureInfos?.ToDictionary(),
|
||||
UrlSegment = cacheNode.Data?.UrlSegment,
|
||||
};
|
||||
|
||||
// TODO: We should probably fix all serialization to only take ContentTypeId, for now it takes an IReadOnlyContentBase
|
||||
// but it is only the content type id that is needed.
|
||||
ContentCacheDataSerializationResult serialized = serializer.Serialize(new ContentSourceDto { ContentTypeId = cacheNode.ContentTypeId, }, contentCacheData, published);
|
||||
|
||||
var dto = new ContentNuDto
|
||||
{
|
||||
NodeId = cacheNode.Id, Published = published, Data = serialized.StringData, RawData = serialized.ByteData,
|
||||
};
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private ContentNuDto GetDtoFromContent(IContentBase content, bool published, IContentCacheDataSerializer serializer)
|
||||
{
|
||||
// should inject these in ctor
|
||||
// BUT for the time being we decide not to support ConvertDbToXml/String
|
||||
// var propertyEditorResolver = PropertyEditorResolver.Current;
|
||||
// var dataTypeService = ApplicationContext.Current.Services.DataTypeService;
|
||||
var propertyData = new Dictionary<string, PropertyData[]>();
|
||||
foreach (IProperty prop in content.Properties)
|
||||
{
|
||||
var pdatas = new List<PropertyData>();
|
||||
foreach (IPropertyValue pvalue in prop.Values.OrderBy(x => x.Culture))
|
||||
{
|
||||
// sanitize - properties should be ok but ... never knows
|
||||
if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// note: at service level, invariant is 'null', but here invariant becomes 'string.Empty'
|
||||
var value = published ? pvalue.PublishedValue : pvalue.EditedValue;
|
||||
if (value != null)
|
||||
{
|
||||
pdatas.Add(new PropertyData
|
||||
{
|
||||
Culture = pvalue.Culture ?? string.Empty,
|
||||
Segment = pvalue.Segment ?? string.Empty,
|
||||
Value = value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
propertyData[prop.Alias] = pdatas.ToArray();
|
||||
}
|
||||
|
||||
var cultureData = new Dictionary<string, CultureVariation>();
|
||||
|
||||
// sanitize - names should be ok but ... never knows
|
||||
if (content.ContentType.VariesByCulture())
|
||||
{
|
||||
ContentCultureInfosCollection? infos = content is IContent document
|
||||
? published
|
||||
? document.PublishCultureInfos
|
||||
: document.CultureInfos
|
||||
: content.CultureInfos;
|
||||
|
||||
// ReSharper disable once UseDeconstruction
|
||||
if (infos is not null)
|
||||
{
|
||||
foreach (ContentCultureInfos cultureInfo in infos)
|
||||
{
|
||||
var cultureIsDraft = !published && content is IContent d && d.IsCultureEdited(cultureInfo.Culture);
|
||||
cultureData[cultureInfo.Culture] = new CultureVariation
|
||||
{
|
||||
Name = cultureInfo.Name,
|
||||
UrlSegment =
|
||||
content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, cultureInfo.Culture),
|
||||
Date = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue,
|
||||
IsDraft = cultureIsDraft,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the dictionary that will be serialized
|
||||
var contentCacheData = new ContentCacheDataModel
|
||||
{
|
||||
PropertyData = propertyData,
|
||||
CultureData = cultureData,
|
||||
UrlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders),
|
||||
};
|
||||
|
||||
ContentCacheDataSerializationResult serialized =
|
||||
serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData, published);
|
||||
|
||||
var dto = new ContentNuDto
|
||||
{
|
||||
NodeId = content.Id, Published = published, Data = serialized.StringData, RawData = serialized.ByteData,
|
||||
};
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
// we want arrays, we want them all loaded, not an enumerable
|
||||
private Sql<ISqlContext> SqlContentSourcesSelect(Func<ISqlContext, Sql<ISqlContext>>? joins = null)
|
||||
{
|
||||
SqlTemplate sqlTemplate = SqlContext.Templates.Get(
|
||||
Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesSelect,
|
||||
tsql =>
|
||||
tsql.Select<NodeDto>(
|
||||
x => Alias(x.NodeId, "Id"),
|
||||
x => Alias(x.UniqueId, "Key"),
|
||||
x => Alias(x.Level, "Level"),
|
||||
x => Alias(x.Path, "Path"),
|
||||
x => Alias(x.SortOrder, "SortOrder"),
|
||||
x => Alias(x.ParentId, "ParentId"),
|
||||
x => Alias(x.CreateDate, "CreateDate"),
|
||||
x => Alias(x.UserId, "CreatorId"))
|
||||
.AndSelect<ContentDto>(x => Alias(x.ContentTypeId, "ContentTypeId"))
|
||||
.AndSelect<DocumentDto>(x => Alias(x.Published, "Published"), x => Alias(x.Edited, "Edited"))
|
||||
.AndSelect<ContentVersionDto>(
|
||||
x => Alias(x.Id, "VersionId"),
|
||||
x => Alias(x.Text, "EditName"),
|
||||
x => Alias(x.VersionDate, "EditVersionDate"),
|
||||
x => Alias(x.UserId, "EditWriterId"))
|
||||
.AndSelect<DocumentVersionDto>(x => Alias(x.TemplateId, "EditTemplateId"))
|
||||
.AndSelect<ContentVersionDto>(
|
||||
"pcver",
|
||||
x => Alias(x.Id, "PublishedVersionId"),
|
||||
x => Alias(x.Text, "PubName"),
|
||||
x => Alias(x.VersionDate, "PubVersionDate"),
|
||||
x => Alias(x.UserId, "PubWriterId"))
|
||||
.AndSelect<DocumentVersionDto>("pdver", x => Alias(x.TemplateId, "PubTemplateId"))
|
||||
.AndSelect<ContentNuDto>("nuEdit", x => Alias(x.Data, "EditData"))
|
||||
.AndSelect<ContentNuDto>("nuPub", x => Alias(x.Data, "PubData"))
|
||||
.AndSelect<ContentNuDto>("nuEdit", x => Alias(x.RawData, "EditDataRaw"))
|
||||
.AndSelect<ContentNuDto>("nuPub", x => Alias(x.RawData, "PubDataRaw"))
|
||||
.From<NodeDto>());
|
||||
|
||||
Sql<ISqlContext>? sql = sqlTemplate.Sql();
|
||||
|
||||
// TODO: I'm unsure how we can format the below into SQL templates also because right.Current and right.Published end up being parameters
|
||||
if (joins != null)
|
||||
{
|
||||
sql = sql.Append(joins(sql.SqlContext));
|
||||
}
|
||||
|
||||
sql = sql
|
||||
.InnerJoin<ContentDto>().On<NodeDto, ContentDto>((left, right) => left.NodeId == right.NodeId)
|
||||
.InnerJoin<DocumentDto>().On<NodeDto, DocumentDto>((left, right) => left.NodeId == right.NodeId)
|
||||
.InnerJoin<ContentVersionDto>()
|
||||
.On<NodeDto, ContentVersionDto>((left, right) => left.NodeId == right.NodeId && right.Current)
|
||||
.InnerJoin<DocumentVersionDto>()
|
||||
.On<ContentVersionDto, DocumentVersionDto>((left, right) => left.Id == right.Id)
|
||||
.LeftJoin<ContentVersionDto>(
|
||||
j =>
|
||||
j.InnerJoin<DocumentVersionDto>("pdver")
|
||||
.On<ContentVersionDto, DocumentVersionDto>(
|
||||
(left, right) => left.Id == right.Id && right.Published == true, "pcver", "pdver"),
|
||||
"pcver")
|
||||
.On<NodeDto, ContentVersionDto>((left, right) => left.NodeId == right.NodeId, aliasRight: "pcver")
|
||||
.LeftJoin<ContentNuDto>("nuEdit").On<NodeDto, ContentNuDto>(
|
||||
(left, right) => left.NodeId == right.NodeId && right.Published == false, aliasRight: "nuEdit")
|
||||
.LeftJoin<ContentNuDto>("nuPub").On<NodeDto, ContentNuDto>(
|
||||
(left, right) => left.NodeId == right.NodeId && right.Published == true, aliasRight: "nuPub");
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
private Sql<ISqlContext> SqlContentSourcesSelectUmbracoNodeJoin(ISqlContext sqlContext)
|
||||
{
|
||||
ISqlSyntaxProvider syntax = sqlContext.SqlSyntax;
|
||||
|
||||
SqlTemplate sqlTemplate = sqlContext.Templates.Get(
|
||||
Constants.SqlTemplates.NuCacheDatabaseDataSource.SourcesSelectUmbracoNodeJoin, builder =>
|
||||
builder.InnerJoin<NodeDto>("x")
|
||||
.On<NodeDto, NodeDto>(
|
||||
(left, right) => left.NodeId == right.NodeId ||
|
||||
SqlText<bool>(left.Path, right.Path,
|
||||
(lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"),
|
||||
aliasRight: "x"));
|
||||
|
||||
Sql<ISqlContext> sql = sqlTemplate.Sql();
|
||||
return sql;
|
||||
}
|
||||
|
||||
private Sql<ISqlContext> SqlWhereNodeId(ISqlContext sqlContext, int id)
|
||||
{
|
||||
ISqlSyntaxProvider syntax = sqlContext.SqlSyntax;
|
||||
|
||||
SqlTemplate sqlTemplate = sqlContext.Templates.Get(
|
||||
Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeId,
|
||||
builder =>
|
||||
builder.Where<NodeDto>(x => x.NodeId == SqlTemplate.Arg<int>("id")));
|
||||
|
||||
Sql<ISqlContext> sql = sqlTemplate.Sql(id);
|
||||
return sql;
|
||||
}
|
||||
|
||||
private Sql<ISqlContext> SqlOrderByLevelIdSortOrder(ISqlContext sqlContext)
|
||||
{
|
||||
ISqlSyntaxProvider syntax = sqlContext.SqlSyntax;
|
||||
|
||||
SqlTemplate sqlTemplate = sqlContext.Templates.Get(
|
||||
Constants.SqlTemplates.NuCacheDatabaseDataSource.OrderByLevelIdSortOrder, s =>
|
||||
s.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder));
|
||||
|
||||
Sql<ISqlContext> sql = sqlTemplate.Sql();
|
||||
return sql;
|
||||
}
|
||||
|
||||
private Sql<ISqlContext> SqlObjectTypeNotTrashed(ISqlContext sqlContext, Guid nodeObjectType)
|
||||
{
|
||||
ISqlSyntaxProvider syntax = sqlContext.SqlSyntax;
|
||||
|
||||
SqlTemplate sqlTemplate = sqlContext.Templates.Get(
|
||||
Constants.SqlTemplates.NuCacheDatabaseDataSource.ObjectTypeNotTrashedFilter, s =>
|
||||
s.Where<NodeDto>(x =>
|
||||
x.NodeObjectType == SqlTemplate.Arg<Guid?>("nodeObjectType") &&
|
||||
x.Trashed == SqlTemplate.Arg<bool>("trashed")));
|
||||
|
||||
Sql<ISqlContext> sql = sqlTemplate.Sql(nodeObjectType, false);
|
||||
return sql;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a slightly more optimized query to use for the document counting when paging over the content sources
|
||||
/// </summary>
|
||||
/// <param name="joins"></param>
|
||||
/// <returns></returns>
|
||||
private Sql<ISqlContext> SqlContentSourcesCount(Func<ISqlContext, Sql<ISqlContext>>? joins = null)
|
||||
{
|
||||
SqlTemplate sqlTemplate = SqlContext.Templates.Get(
|
||||
Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesCount, tsql =>
|
||||
tsql.Select<NodeDto>(x => Alias(x.NodeId, "Id"))
|
||||
.From<NodeDto>()
|
||||
.InnerJoin<ContentDto>().On<NodeDto, ContentDto>((left, right) => left.NodeId == right.NodeId)
|
||||
.InnerJoin<DocumentDto>().On<NodeDto, DocumentDto>((left, right) => left.NodeId == right.NodeId));
|
||||
|
||||
Sql<ISqlContext>? sql = sqlTemplate.Sql();
|
||||
|
||||
if (joins != null)
|
||||
{
|
||||
sql = sql.Append(joins(sql.SqlContext));
|
||||
}
|
||||
|
||||
// TODO: We can't use a template with this one because of the 'right.Current' and 'right.Published' ends up being a parameter so not sure how we can do that
|
||||
sql = sql
|
||||
.InnerJoin<ContentVersionDto>()
|
||||
.On<NodeDto, ContentVersionDto>((left, right) => left.NodeId == right.NodeId && right.Current)
|
||||
.InnerJoin<DocumentVersionDto>()
|
||||
.On<ContentVersionDto, DocumentVersionDto>((left, right) => left.Id == right.Id)
|
||||
.LeftJoin<ContentVersionDto>(
|
||||
j =>
|
||||
j.InnerJoin<DocumentVersionDto>("pdver")
|
||||
.On<ContentVersionDto, DocumentVersionDto>(
|
||||
(left, right) => left.Id == right.Id && right.Published,
|
||||
"pcver",
|
||||
"pdver"),
|
||||
"pcver")
|
||||
.On<NodeDto, ContentVersionDto>((left, right) => left.NodeId == right.NodeId, aliasRight: "pcver");
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
private Sql<ISqlContext> SqlMediaSourcesSelect(Func<ISqlContext, Sql<ISqlContext>>? joins = null)
|
||||
{
|
||||
SqlTemplate sqlTemplate = SqlContext.Templates.Get(
|
||||
Constants.SqlTemplates.NuCacheDatabaseDataSource.MediaSourcesSelect, tsql =>
|
||||
tsql.Select<NodeDto>(
|
||||
x => Alias(x.NodeId, "Id"),
|
||||
x => Alias(x.UniqueId, "Key"),
|
||||
x => Alias(x.Level, "Level"),
|
||||
x => Alias(x.Path, "Path"),
|
||||
x => Alias(x.SortOrder, "SortOrder"),
|
||||
x => Alias(x.ParentId, "ParentId"),
|
||||
x => Alias(x.CreateDate, "CreateDate"),
|
||||
x => Alias(x.UserId, "CreatorId"))
|
||||
.AndSelect<ContentDto>(x => Alias(x.ContentTypeId, "ContentTypeId"))
|
||||
.AndSelect<ContentVersionDto>(
|
||||
x => Alias(x.Id, "VersionId"),
|
||||
x => Alias(x.Text, "EditName"),
|
||||
x => Alias(x.VersionDate, "EditVersionDate"),
|
||||
x => Alias(x.UserId, "EditWriterId"))
|
||||
.AndSelect<ContentNuDto>("nuEdit", x => Alias(x.Data, "EditData"))
|
||||
.AndSelect<ContentNuDto>("nuEdit", x => Alias(x.RawData, "EditDataRaw"))
|
||||
.From<NodeDto>());
|
||||
|
||||
Sql<ISqlContext>? sql = sqlTemplate.Sql();
|
||||
|
||||
if (joins != null)
|
||||
{
|
||||
sql = sql.Append(joins(sql.SqlContext));
|
||||
}
|
||||
|
||||
// TODO: We can't use a template with this one because of the 'right.Published' ends up being a parameter so not sure how we can do that
|
||||
sql = sql
|
||||
.InnerJoin<ContentDto>().On<NodeDto, ContentDto>((left, right) => left.NodeId == right.NodeId)
|
||||
.InnerJoin<ContentVersionDto>()
|
||||
.On<NodeDto, ContentVersionDto>((left, right) => left.NodeId == right.NodeId && right.Current)
|
||||
.LeftJoin<ContentNuDto>("nuEdit")
|
||||
.On<NodeDto, ContentNuDto>(
|
||||
(left, right) => left.NodeId == right.NodeId && !right.Published,
|
||||
aliasRight: "nuEdit");
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
private ContentCacheNode CreateContentNodeKit(ContentSourceDto dto, IContentCacheDataSerializer serializer, bool preview)
|
||||
{
|
||||
if (preview)
|
||||
{
|
||||
if (dto.EditData == null && dto.EditDataRaw == null)
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
throw new InvalidOperationException("Missing cmsContentNu edited content for node " + dto.Id +
|
||||
", consider rebuilding.");
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.",
|
||||
dto.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool published = false;
|
||||
ContentCacheDataModel? deserializedDraftContent =
|
||||
serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published);
|
||||
var draftContentData = new ContentData(
|
||||
dto.EditName,
|
||||
null,
|
||||
dto.VersionId,
|
||||
dto.EditVersionDate,
|
||||
dto.CreatorId,
|
||||
dto.EditTemplateId == 0 ? null : dto.EditTemplateId,
|
||||
published,
|
||||
deserializedDraftContent?.PropertyData,
|
||||
deserializedDraftContent?.CultureData);
|
||||
|
||||
return new ContentCacheNode
|
||||
{
|
||||
Id = dto.Id,
|
||||
Key = dto.Key,
|
||||
SortOrder = dto.SortOrder,
|
||||
CreateDate = dto.CreateDate,
|
||||
CreatorId = dto.CreatorId,
|
||||
ContentTypeId = dto.ContentTypeId,
|
||||
Data = draftContentData,
|
||||
IsDraft = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.PubData == null && dto.PubDataRaw == null)
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
throw new InvalidOperationException("Missing cmsContentNu published content for node " + dto.Id +
|
||||
", consider rebuilding.");
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Missing cmsContentNu published content for node {NodeId}, consider rebuilding.",
|
||||
dto.Id);
|
||||
}
|
||||
|
||||
ContentCacheDataModel? deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw, true);
|
||||
var publishedContentData = new ContentData(
|
||||
dto.PubName,
|
||||
null,
|
||||
dto.VersionId,
|
||||
dto.PubVersionDate,
|
||||
dto.CreatorId,
|
||||
dto.EditTemplateId == 0 ? null : dto.EditTemplateId,
|
||||
true,
|
||||
deserializedContent?.PropertyData,
|
||||
deserializedContent?.CultureData);
|
||||
|
||||
return new ContentCacheNode
|
||||
{
|
||||
Id = dto.Id,
|
||||
Key = dto.Key,
|
||||
SortOrder = dto.SortOrder,
|
||||
CreateDate = dto.CreateDate,
|
||||
CreatorId = dto.CreatorId,
|
||||
ContentTypeId = dto.ContentTypeId,
|
||||
Data = publishedContentData,
|
||||
IsDraft = false,
|
||||
};
|
||||
}
|
||||
|
||||
private ContentCacheNode CreateMediaNodeKit(ContentSourceDto dto, IContentCacheDataSerializer serializer)
|
||||
{
|
||||
if (dto.EditData == null && dto.EditDataRaw == null)
|
||||
{
|
||||
throw new InvalidOperationException("No data for media " + dto.Id);
|
||||
}
|
||||
|
||||
ContentCacheDataModel? deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, true);
|
||||
|
||||
var publishedContentData = new ContentData(
|
||||
dto.EditName,
|
||||
null,
|
||||
dto.VersionId,
|
||||
dto.EditVersionDate,
|
||||
dto.CreatorId,
|
||||
dto.EditTemplateId == 0 ? null : dto.EditTemplateId,
|
||||
true,
|
||||
deserializedMedia?.PropertyData,
|
||||
deserializedMedia?.CultureData);
|
||||
|
||||
return new ContentCacheNode
|
||||
{
|
||||
Id = dto.Id,
|
||||
Key = dto.Key,
|
||||
SortOrder = dto.SortOrder,
|
||||
CreateDate = dto.CreateDate,
|
||||
CreatorId = dto.CreatorId,
|
||||
ContentTypeId = dto.ContentTypeId,
|
||||
Data = publishedContentData,
|
||||
IsDraft = false,
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<ContentSourceDto> GetContentNodeDtos(Sql<ISqlContext> sql)
|
||||
{
|
||||
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
|
||||
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
|
||||
// QueryPaged is very slow on large sites however, so use fetch if UsePagedSqlQuery is disabled.
|
||||
IEnumerable<ContentSourceDto> dtos;
|
||||
if (_nucacheSettings.Value.UsePagedSqlQuery)
|
||||
{
|
||||
// Use a more efficient COUNT query
|
||||
Sql<ISqlContext>? sqlCountQuery = SqlContentSourcesCount()
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document));
|
||||
|
||||
Sql<ISqlContext>? sqlCount =
|
||||
SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
|
||||
|
||||
dtos = Database.QueryPaged<ContentSourceDto>(_nucacheSettings.Value.SqlPageSize, sql, sqlCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
dtos = Database.Fetch<ContentSourceDto>(sql);
|
||||
}
|
||||
|
||||
return dtos;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Persistence;
|
||||
|
||||
internal interface IDatabaseCacheRepository
|
||||
{
|
||||
Task DeleteContentItemAsync(int id);
|
||||
|
||||
Task<ContentCacheNode?> GetContentSourceAsync(int id, bool preview = false);
|
||||
|
||||
Task<ContentCacheNode?> GetMediaSourceAsync(int id);
|
||||
|
||||
IEnumerable<ContentCacheNode> GetContentByContentTypeKey(IEnumerable<Guid> keys);
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the nucache database row for the given cache node />
|
||||
/// </summary>
|
||||
/// <returns><placeholder>A <see cref="Task"/> representing the asynchronous operation.</placeholder></returns>
|
||||
Task RefreshContentAsync(ContentCacheNode contentCacheNode, PublishedState publishedState);
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the nucache database row for the given cache node />
|
||||
/// </summary>
|
||||
/// <returns><placeholder>A <see cref="Task"/> representing the asynchronous operation.</placeholder></returns>
|
||||
Task RefreshMediaAsync(ContentCacheNode contentCacheNode);
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the caches for content, media and/or members based on the content type ids specified
|
||||
/// </summary>
|
||||
/// <param name="contentTypeIds">
|
||||
/// If not null will process content for the matching content types, if empty will process all
|
||||
/// content
|
||||
/// </param>
|
||||
/// <param name="mediaTypeIds">
|
||||
/// If not null will process content for the matching media types, if empty will process all
|
||||
/// media
|
||||
/// </param>
|
||||
/// <param name="memberTypeIds">
|
||||
/// If not null will process content for the matching members types, if empty will process all
|
||||
/// members
|
||||
/// </param>
|
||||
void Rebuild(
|
||||
IReadOnlyCollection<int>? contentTypeIds = null,
|
||||
IReadOnlyCollection<int>? mediaTypeIds = null,
|
||||
IReadOnlyCollection<int>? memberTypeIds = null);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the content cache by asserting that every document should have a corresponding row for edited properties and if published,
|
||||
/// may have a corresponding row for published properties
|
||||
/// </summary>
|
||||
bool VerifyContentDbCache();
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the caches for content, media and/or members based on the content type ids specified
|
||||
/// </summary>
|
||||
bool VerifyMediaDbCache();
|
||||
}
|
||||
43
src/Umbraco.PublishedCache.HybridCache/PropertyData.cs
Normal file
43
src/Umbraco.PublishedCache.HybridCache/PropertyData.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
// This is for cache performance reasons, see https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0#reuse-objects
|
||||
[ImmutableObject(true)]
|
||||
[DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys
|
||||
public sealed class PropertyData
|
||||
{
|
||||
private string? _culture;
|
||||
private string? _segment;
|
||||
|
||||
[DataMember(Order = 0)]
|
||||
[JsonConverter(typeof(JsonStringInternConverter))]
|
||||
[DefaultValue("")]
|
||||
[JsonPropertyName("c")]
|
||||
public string? Culture
|
||||
{
|
||||
get => _culture;
|
||||
set => _culture =
|
||||
value ?? throw new ArgumentNullException(
|
||||
nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null
|
||||
}
|
||||
|
||||
[DataMember(Order = 1)]
|
||||
[JsonConverter(typeof(JsonStringInternConverter))]
|
||||
[DefaultValue("")]
|
||||
[JsonPropertyName("s")]
|
||||
public string? Segment
|
||||
{
|
||||
get => _segment;
|
||||
set => _segment =
|
||||
value ?? throw new ArgumentNullException(
|
||||
nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null
|
||||
}
|
||||
|
||||
[DataMember(Order = 2)]
|
||||
[JsonPropertyName("v")]
|
||||
public object? Value { get; set; }
|
||||
}
|
||||
195
src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs
Normal file
195
src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
using Umbraco.Cms.Core.Exceptions;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
internal class PublishedContent : PublishedContentBase
|
||||
{
|
||||
private IPublishedProperty[] _properties;
|
||||
private readonly ContentNode _contentNode;
|
||||
private IReadOnlyDictionary<string, PublishedCultureInfo>? _cultures;
|
||||
private readonly string? _urlSegment;
|
||||
private readonly IReadOnlyDictionary<string, CultureVariation>? _cultureInfos;
|
||||
private readonly string _contentName;
|
||||
private readonly bool _published;
|
||||
|
||||
public PublishedContent(
|
||||
ContentNode contentNode,
|
||||
bool preview,
|
||||
IElementsCache elementsCache,
|
||||
IVariationContextAccessor variationContextAccessor)
|
||||
: base(variationContextAccessor)
|
||||
{
|
||||
VariationContextAccessor = variationContextAccessor;
|
||||
_contentNode = contentNode;
|
||||
ContentData? contentData = preview ? _contentNode.DraftModel : _contentNode.PublishedModel;
|
||||
if (contentData is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(contentData));
|
||||
}
|
||||
|
||||
_cultureInfos = contentData.CultureInfos;
|
||||
_contentName = contentData.Name;
|
||||
_urlSegment = contentData.UrlSegment;
|
||||
_published = contentData.Published;
|
||||
|
||||
var properties = new IPublishedProperty[_contentNode.ContentType.PropertyTypes.Count()];
|
||||
var i = 0;
|
||||
foreach (IPublishedPropertyType propertyType in _contentNode.ContentType.PropertyTypes)
|
||||
{
|
||||
// add one property per property type - this is required, for the indexing to work
|
||||
// if contentData supplies pdatas, use them, else use null
|
||||
contentData.Properties.TryGetValue(propertyType.Alias, out PropertyData[]? propertyDatas); // else will be null
|
||||
properties[i++] = new PublishedProperty(propertyType, this, propertyDatas, elementsCache, propertyType.CacheLevel);
|
||||
}
|
||||
|
||||
_properties = properties;
|
||||
|
||||
Id = contentNode.Id;
|
||||
Key = contentNode.Key;
|
||||
CreatorId = contentNode.CreatorId;
|
||||
CreateDate = contentNode.CreateDate;
|
||||
SortOrder = contentNode.SortOrder;
|
||||
WriterId = contentData.WriterId;
|
||||
TemplateId = contentData.TemplateId;
|
||||
UpdateDate = contentData.VersionDate;
|
||||
}
|
||||
|
||||
public override IPublishedContentType ContentType => _contentNode.ContentType;
|
||||
|
||||
public override Guid Key { get; }
|
||||
|
||||
public override IEnumerable<IPublishedProperty> Properties => _properties;
|
||||
|
||||
public override int Id { get; }
|
||||
|
||||
public override int SortOrder { get; }
|
||||
|
||||
// TODO: Remove path.
|
||||
public override string Path => string.Empty;
|
||||
|
||||
public override int? TemplateId { get; }
|
||||
|
||||
public override int CreatorId { get; }
|
||||
|
||||
public override DateTime CreateDate { get; }
|
||||
|
||||
public override int WriterId { get; }
|
||||
|
||||
public override DateTime UpdateDate { get; }
|
||||
|
||||
public bool IsPreviewing { get; } = false;
|
||||
|
||||
// Needed for publishedProperty
|
||||
internal IVariationContextAccessor VariationContextAccessor { get; }
|
||||
|
||||
public override int Level { get; } = 0;
|
||||
|
||||
public override IEnumerable<IPublishedContent> ChildrenForAllCultures { get; } = Enumerable.Empty<IPublishedContent>();
|
||||
|
||||
public override IPublishedContent? Parent { get; } = null!;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyDictionary<string, PublishedCultureInfo> Cultures
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_cultures != null)
|
||||
{
|
||||
return _cultures;
|
||||
}
|
||||
|
||||
if (!ContentType.VariesByCulture())
|
||||
{
|
||||
return _cultures = new Dictionary<string, PublishedCultureInfo>
|
||||
{
|
||||
{ string.Empty, new PublishedCultureInfo(string.Empty, _contentName, _urlSegment, CreateDate) },
|
||||
};
|
||||
}
|
||||
|
||||
if (_cultureInfos == null)
|
||||
{
|
||||
throw new PanicException("_contentDate.CultureInfos is null.");
|
||||
}
|
||||
|
||||
|
||||
return _cultures = _cultureInfos
|
||||
.ToDictionary(
|
||||
x => x.Key,
|
||||
x => new PublishedCultureInfo(x.Key, x.Value.Name, x.Value.UrlSegment, x.Value.Date),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override PublishedItemType ItemType => _contentNode.ContentType.ItemType;
|
||||
|
||||
public override IPublishedProperty? GetProperty(string alias)
|
||||
{
|
||||
var index = _contentNode.ContentType.GetPropertyIndex(alias);
|
||||
if (index < 0)
|
||||
{
|
||||
return null; // happens when 'alias' does not match a content type property alias
|
||||
}
|
||||
|
||||
// should never happen - properties array must be in sync with property type
|
||||
if (index >= _properties.Length)
|
||||
{
|
||||
throw new IndexOutOfRangeException(
|
||||
"Index points outside the properties array, which means the properties array is corrupt.");
|
||||
}
|
||||
|
||||
IPublishedProperty property = _properties[index];
|
||||
return property;
|
||||
}
|
||||
|
||||
public override bool IsDraft(string? culture = null)
|
||||
{
|
||||
// if this is the 'published' published content, nothing can be draft
|
||||
if (_published)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// not the 'published' published content, and does not vary = must be draft
|
||||
if (!ContentType.VariesByCulture())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// handle context culture
|
||||
culture ??= VariationContextAccessor?.VariationContext?.Culture ?? string.Empty;
|
||||
|
||||
// not the 'published' published content, and varies
|
||||
// = depends on the culture
|
||||
return _cultureInfos is not null && _cultureInfos.TryGetValue(culture, out CultureVariation? cvar) && cvar.IsDraft;
|
||||
}
|
||||
|
||||
public override bool IsPublished(string? culture = null)
|
||||
{
|
||||
// whether we are the 'draft' or 'published' content, need to determine whether
|
||||
// there is a 'published' version for the specified culture (or at all, for
|
||||
// invariant content items)
|
||||
|
||||
// if there is no 'published' published content, no culture can be published
|
||||
if (!_contentNode.HasPublished)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// if there is a 'published' published content, and does not vary = published
|
||||
if (!ContentType.VariesByCulture())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// handle context culture
|
||||
culture ??= VariationContextAccessor.VariationContext?.Culture ?? string.Empty;
|
||||
|
||||
// there is a 'published' published content, and varies
|
||||
// = depends on the culture
|
||||
return _contentNode.HasPublishedCulture(culture);
|
||||
}
|
||||
}
|
||||
38
src/Umbraco.PublishedCache.HybridCache/PublishedMember.cs
Normal file
38
src/Umbraco.PublishedCache.HybridCache/PublishedMember.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
// note
|
||||
// the whole PublishedMember thing should be refactored because as soon as a member
|
||||
// is wrapped on in a model, the inner IMember and all associated properties are lost
|
||||
internal class PublishedMember : PublishedContent, IPublishedMember
|
||||
{
|
||||
private readonly IMember _member;
|
||||
|
||||
public PublishedMember(
|
||||
IMember member,
|
||||
ContentNode contentNode,
|
||||
IElementsCache elementsCache,
|
||||
IVariationContextAccessor variationContextAccessor)
|
||||
: base(contentNode, false, elementsCache, variationContextAccessor) =>
|
||||
_member = member;
|
||||
|
||||
public string Email => _member.Email;
|
||||
|
||||
public string UserName => _member.Username;
|
||||
|
||||
public string? Comments => _member.Comments;
|
||||
|
||||
public bool IsApproved => _member.IsApproved;
|
||||
|
||||
public bool IsLockedOut => _member.IsLockedOut;
|
||||
|
||||
public DateTime? LastLockoutDate => _member.LastLockoutDate;
|
||||
|
||||
public DateTime CreationDate => _member.CreateDate;
|
||||
|
||||
public DateTime? LastLoginDate => _member.LastLoginDate;
|
||||
|
||||
public DateTime? LastPasswordChangedDate => _member.LastPasswordChangeDate;
|
||||
}
|
||||
330
src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs
Normal file
330
src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Collections;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
|
||||
internal class PublishedProperty : PublishedPropertyBase
|
||||
{
|
||||
private readonly PublishedContent _content;
|
||||
private readonly bool _isPreviewing;
|
||||
private readonly IElementsCache _elementsCache;
|
||||
private readonly bool _isMember;
|
||||
private string? _valuesCacheKey;
|
||||
|
||||
// the invariant-neutral source and inter values
|
||||
private readonly object? _sourceValue;
|
||||
private readonly ContentVariation _variations;
|
||||
private readonly ContentVariation _sourceVariations;
|
||||
|
||||
// the variant and non-variant object values
|
||||
private bool _interInitialized;
|
||||
private object? _interValue;
|
||||
private CacheValues? _cacheValues;
|
||||
|
||||
// the variant source and inter values
|
||||
private readonly object _locko = new();
|
||||
private ConcurrentDictionary<CompositeStringStringKey, SourceInterValue>? _sourceValues;
|
||||
|
||||
// initializes a published content property with a value
|
||||
public PublishedProperty(
|
||||
IPublishedPropertyType propertyType,
|
||||
PublishedContent content,
|
||||
PropertyData[]? sourceValues,
|
||||
IElementsCache elementsElementsCache,
|
||||
PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element)
|
||||
: base(propertyType, referenceCacheLevel)
|
||||
{
|
||||
if (sourceValues != null)
|
||||
{
|
||||
foreach (PropertyData sourceValue in sourceValues)
|
||||
{
|
||||
if (sourceValue.Culture == string.Empty && sourceValue.Segment == string.Empty)
|
||||
{
|
||||
_sourceValue = sourceValue.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureSourceValuesInitialized();
|
||||
|
||||
_sourceValues![new CompositeStringStringKey(sourceValue.Culture, sourceValue.Segment)]
|
||||
= new SourceInterValue
|
||||
{
|
||||
Culture = sourceValue.Culture,
|
||||
Segment = sourceValue.Segment,
|
||||
SourceValue = sourceValue.Value,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_content = content;
|
||||
_isPreviewing = content.IsPreviewing;
|
||||
_isMember = content.ContentType.ItemType == PublishedItemType.Member;
|
||||
_elementsCache = elementsElementsCache;
|
||||
|
||||
// this variable is used for contextualizing the variation level when calculating property values.
|
||||
// it must be set to the union of variance (the combination of content type and property type variance).
|
||||
_variations = propertyType.Variations | content.ContentType.Variations;
|
||||
_sourceVariations = propertyType.Variations;
|
||||
}
|
||||
|
||||
// used to cache the CacheValues of this property
|
||||
internal string ValuesCacheKey => _valuesCacheKey ??= PropertyCacheValues(_content.Key, Alias, _isPreviewing);
|
||||
|
||||
private string PropertyCacheValues(Guid contentUid, string typeAlias, bool previewing)
|
||||
{
|
||||
if (previewing)
|
||||
{
|
||||
return "Cache.Property.CacheValues[D:" + contentUid + ":" + typeAlias + "]";
|
||||
}
|
||||
|
||||
return "Cache.Property.CacheValues[P:" + contentUid + ":" + typeAlias + "]";
|
||||
}
|
||||
|
||||
// determines whether a property has value
|
||||
public override bool HasValue(string? culture = null, string? segment = null)
|
||||
{
|
||||
_content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment);
|
||||
|
||||
var value = GetSourceValue(culture, segment);
|
||||
var hasValue = PropertyType.IsValue(value, PropertyValueLevel.Source);
|
||||
if (hasValue.HasValue)
|
||||
{
|
||||
return hasValue.Value;
|
||||
}
|
||||
|
||||
return PropertyType.IsValue(GetInterValue(culture, segment), PropertyValueLevel.Object) ?? false;
|
||||
}
|
||||
|
||||
public override object? GetSourceValue(string? culture = null, string? segment = null)
|
||||
{
|
||||
_content.VariationContextAccessor.ContextualizeVariation(_sourceVariations, _content.Id, ref culture, ref segment);
|
||||
|
||||
// source values are tightly bound to the property/schema culture and segment configurations, so we need to
|
||||
// sanitize the contextualized culture/segment states before using them to access the source values.
|
||||
culture = _sourceVariations.VariesByCulture() ? culture : string.Empty;
|
||||
segment = _sourceVariations.VariesBySegment() ? segment : string.Empty;
|
||||
|
||||
if (culture == string.Empty && segment == string.Empty)
|
||||
{
|
||||
return _sourceValue;
|
||||
}
|
||||
|
||||
if (_sourceValues == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _sourceValues.TryGetValue(
|
||||
new CompositeStringStringKey(culture, segment),
|
||||
out SourceInterValue? sourceValue)
|
||||
? sourceValue.SourceValue
|
||||
: null;
|
||||
}
|
||||
|
||||
private object? GetInterValue(string? culture, string? segment)
|
||||
{
|
||||
if (culture is "" && segment is "")
|
||||
{
|
||||
if (_interInitialized)
|
||||
{
|
||||
return _interValue;
|
||||
}
|
||||
|
||||
_interValue = PropertyType.ConvertSourceToInter(_content, _sourceValue, _isPreviewing);
|
||||
_interInitialized = true;
|
||||
return _interValue;
|
||||
}
|
||||
|
||||
return PropertyType.ConvertSourceToInter(_content, GetSourceValue(culture, segment), _isPreviewing);
|
||||
}
|
||||
|
||||
public override object? GetValue(string? culture = null, string? segment = null)
|
||||
{
|
||||
_content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment);
|
||||
|
||||
object? value;
|
||||
CacheValue cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment);
|
||||
|
||||
// initial reference cache level always is .Content
|
||||
const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element;
|
||||
|
||||
if (cacheValues.ObjectInitialized)
|
||||
{
|
||||
return cacheValues.ObjectValue;
|
||||
}
|
||||
|
||||
cacheValues.ObjectValue = PropertyType.ConvertInterToObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing);
|
||||
cacheValues.ObjectInitialized = true;
|
||||
value = cacheValues.ObjectValue;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel)
|
||||
{
|
||||
CacheValues cacheValues;
|
||||
IAppCache? cache;
|
||||
switch (cacheLevel)
|
||||
{
|
||||
case PropertyCacheLevel.None:
|
||||
// never cache anything
|
||||
cacheValues = new CacheValues();
|
||||
break;
|
||||
case PropertyCacheLevel.Snapshot: // Snapshot is obsolete, so for now treat as element
|
||||
case PropertyCacheLevel.Element:
|
||||
// cache within the property object itself, ie within the content object
|
||||
cacheValues = _cacheValues ??= new CacheValues();
|
||||
break;
|
||||
case PropertyCacheLevel.Elements:
|
||||
// cache within the elements cache, unless previewing, then use the snapshot or
|
||||
// elements cache (if we don't want to pollute the elements cache with short-lived
|
||||
// data) depending on settings
|
||||
// for members, always cache in the snapshot cache - never pollute elements cache
|
||||
cache = _isMember == false ? _elementsCache : null;
|
||||
cacheValues = GetCacheValues(cache);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException("Invalid cache level.");
|
||||
}
|
||||
|
||||
return cacheValues;
|
||||
}
|
||||
|
||||
private CacheValues GetCacheValues(IAppCache? cache)
|
||||
{
|
||||
// no cache, don't cache
|
||||
if (cache == null)
|
||||
{
|
||||
return new CacheValues();
|
||||
}
|
||||
|
||||
return (CacheValues)cache.Get(ValuesCacheKey, () => new CacheValues())!;
|
||||
}
|
||||
|
||||
public override object? GetDeliveryApiValue(bool expanding, string? culture = null, string? segment = null)
|
||||
{
|
||||
_content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment);
|
||||
|
||||
object? value;
|
||||
CacheValue cacheValues = GetCacheValues(expanding ? PropertyType.DeliveryApiCacheLevelForExpansion : PropertyType.DeliveryApiCacheLevel).For(culture, segment);
|
||||
|
||||
|
||||
// initial reference cache level always is .Content
|
||||
const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element;
|
||||
|
||||
object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing, expanding);
|
||||
value = expanding
|
||||
? GetDeliveryApiExpandedObject(cacheValues, GetDeliveryApiObject)
|
||||
: GetDeliveryApiDefaultObject(cacheValues, GetDeliveryApiObject);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private object? GetDeliveryApiDefaultObject(CacheValue cacheValues, Func<object?> getValue)
|
||||
{
|
||||
if (cacheValues.DeliveryApiDefaultObjectInitialized == false)
|
||||
{
|
||||
cacheValues.DeliveryApiDefaultObjectValue = getValue();
|
||||
cacheValues.DeliveryApiDefaultObjectInitialized = true;
|
||||
}
|
||||
|
||||
return cacheValues.DeliveryApiDefaultObjectValue;
|
||||
}
|
||||
|
||||
private object? GetDeliveryApiExpandedObject(CacheValue cacheValues, Func<object?> getValue)
|
||||
{
|
||||
if (cacheValues.DeliveryApiExpandedObjectInitialized == false)
|
||||
{
|
||||
cacheValues.DeliveryApiExpandedObjectValue = getValue();
|
||||
cacheValues.DeliveryApiExpandedObjectInitialized = true;
|
||||
}
|
||||
|
||||
return cacheValues.DeliveryApiExpandedObjectValue;
|
||||
}
|
||||
|
||||
private class SourceInterValue
|
||||
{
|
||||
private string? _culture;
|
||||
private string? _segment;
|
||||
|
||||
public string? Culture
|
||||
{
|
||||
get => _culture;
|
||||
internal set => _culture = value?.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public string? Segment
|
||||
{
|
||||
get => _segment;
|
||||
internal set => _segment = value?.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public object? SourceValue { get; set; }
|
||||
}
|
||||
|
||||
private class CacheValues : CacheValue
|
||||
{
|
||||
private readonly object _locko = new();
|
||||
private ConcurrentDictionary<CompositeStringStringKey, CacheValue>? _values;
|
||||
|
||||
public CacheValue For(string? culture, string? segment)
|
||||
{
|
||||
if (culture == string.Empty && segment == string.Empty)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
if (_values == null)
|
||||
{
|
||||
lock (_locko)
|
||||
{
|
||||
_values ??= InitializeConcurrentDictionary<CompositeStringStringKey, CacheValue>();
|
||||
}
|
||||
}
|
||||
|
||||
var k = new CompositeStringStringKey(culture, segment);
|
||||
|
||||
CacheValue value = _values.GetOrAdd(k, _ => new CacheValue());
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheValue
|
||||
{
|
||||
public bool ObjectInitialized { get; set; }
|
||||
|
||||
public object? ObjectValue { get; set; }
|
||||
|
||||
public bool DeliveryApiDefaultObjectInitialized { get; set; }
|
||||
|
||||
public object? DeliveryApiDefaultObjectValue { get; set; }
|
||||
|
||||
public bool DeliveryApiExpandedObjectInitialized { get; set; }
|
||||
|
||||
public object? DeliveryApiExpandedObjectValue { get; set; }
|
||||
}
|
||||
|
||||
private static ConcurrentDictionary<TKey, TValue> InitializeConcurrentDictionary<TKey, TValue>()
|
||||
where TKey : notnull
|
||||
=> new(-1, 5);
|
||||
|
||||
private void EnsureSourceValuesInitialized()
|
||||
{
|
||||
if (_sourceValues is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_locko)
|
||||
{
|
||||
_sourceValues ??= InitializeConcurrentDictionary<CompositeStringStringKey, SourceInterValue>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using MessagePack;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// The content model stored in the content cache database table serialized as JSON
|
||||
/// </summary>
|
||||
[DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys
|
||||
public sealed class ContentCacheDataModel
|
||||
{
|
||||
[DataMember(Order = 0)]
|
||||
[JsonPropertyName("pd")]
|
||||
[JsonConverter(typeof(JsonDictionaryStringInternIgnoreCaseConverter<PropertyData[]>))]
|
||||
[MessagePackFormatter(typeof(MessagePackDictionaryStringInternIgnoreCaseFormatter<PropertyData[]>))]
|
||||
public Dictionary<string, PropertyData[]>? PropertyData { get; set; }
|
||||
|
||||
[DataMember(Order = 1)]
|
||||
[JsonPropertyName("cd")]
|
||||
[JsonConverter(typeof(JsonDictionaryStringInternIgnoreCaseConverter<CultureVariation>))]
|
||||
[MessagePackFormatter(typeof(MessagePackDictionaryStringInternIgnoreCaseFormatter<CultureVariation>))]
|
||||
public Dictionary<string, CultureVariation>? CultureData { get; set; }
|
||||
|
||||
// TODO: Remove this when routing cache is in place
|
||||
[DataMember(Order = 2)]
|
||||
[JsonPropertyName("us")]
|
||||
public string? UrlSegment { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// The serialization result from <see cref="IContentCacheDataSerializer" /> for which the serialized value
|
||||
/// will be either a string or a byte[]
|
||||
/// </summary>
|
||||
public struct ContentCacheDataSerializationResult : IEquatable<ContentCacheDataSerializationResult>
|
||||
{
|
||||
public ContentCacheDataSerializationResult(string? stringData, byte[]? byteData)
|
||||
{
|
||||
StringData = stringData;
|
||||
ByteData = byteData;
|
||||
}
|
||||
|
||||
public string? StringData { get; }
|
||||
|
||||
public byte[]? ByteData { get; }
|
||||
|
||||
public static bool operator ==(ContentCacheDataSerializationResult left, ContentCacheDataSerializationResult right)
|
||||
=> left.Equals(right);
|
||||
|
||||
public static bool operator !=(ContentCacheDataSerializationResult left, ContentCacheDataSerializationResult right)
|
||||
=> !(left == right);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is ContentCacheDataSerializationResult result && Equals(result);
|
||||
|
||||
public bool Equals(ContentCacheDataSerializationResult other)
|
||||
=> StringData == other.StringData &&
|
||||
EqualityComparer<byte[]>.Default.Equals(ByteData, other.ByteData);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashCode = 1910544615;
|
||||
if (StringData is not null)
|
||||
{
|
||||
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(StringData);
|
||||
}
|
||||
|
||||
if (ByteData is not null)
|
||||
{
|
||||
hashCode = (hashCode * -1521134295) + EqualityComparer<byte[]>.Default.GetHashCode(ByteData);
|
||||
}
|
||||
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
|
||||
[Flags]
|
||||
public enum ContentCacheDataSerializerEntityType
|
||||
{
|
||||
Document = 1,
|
||||
Media = 2,
|
||||
Member = 4,
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes/Deserializes <see cref="ContentCacheDataModel" /> document to the SQL Database as a string
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Resolved from the <see cref="IContentCacheDataSerializerFactory" />. This cannot be resolved from DI.
|
||||
/// </remarks>
|
||||
internal interface IContentCacheDataSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Deserialize the data into a <see cref="ContentCacheDataModel" />
|
||||
/// </summary>
|
||||
ContentCacheDataModel? Deserialize(IReadOnlyContentBase content, string? stringData, byte[]? byteData, bool published);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the <see cref="ContentCacheDataModel" />
|
||||
/// </summary>
|
||||
ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
|
||||
internal interface IContentCacheDataSerializerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or creates a new instance of <see cref="IContentCacheDataSerializer" />
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This method may return the same instance, however this depends on the state of the application and if any
|
||||
/// underlying data has changed.
|
||||
/// This method may also be used to initialize anything before a serialization/deserialization session occurs.
|
||||
/// </remarks>
|
||||
IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
|
||||
internal class JsonContentNestedDataSerializer : IContentCacheDataSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public ContentCacheDataModel? Deserialize(
|
||||
IReadOnlyContentBase content,
|
||||
string? stringData,
|
||||
byte[]? byteData,
|
||||
bool published)
|
||||
{
|
||||
if (stringData == null && byteData != null)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization");
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<ContentCacheDataModel>(stringData!, _jsonSerializerOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ContentCacheDataSerializationResult Serialize(
|
||||
IReadOnlyContentBase content,
|
||||
ContentCacheDataModel model,
|
||||
bool published)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(model, _jsonSerializerOptions);
|
||||
return new ContentCacheDataSerializationResult(json, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
|
||||
internal class JsonContentNestedDataSerializerFactory : IContentCacheDataSerializerFactory
|
||||
{
|
||||
private readonly Lazy<JsonContentNestedDataSerializer> _serializer = new();
|
||||
|
||||
public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types) => _serializer.Value;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using K4os.Compression.LZ4;
|
||||
using Umbraco.Cms.Core.Exceptions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Lazily decompresses a LZ4 Pickler compressed UTF8 string
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Display}")]
|
||||
internal struct LazyCompressedString
|
||||
{
|
||||
private readonly object _locker;
|
||||
private byte[]? _bytes;
|
||||
private string? _str;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="bytes">LZ4 Pickle compressed UTF8 String</param>
|
||||
public LazyCompressedString(byte[] bytes)
|
||||
{
|
||||
_locker = new object();
|
||||
_bytes = bytes;
|
||||
_str = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to display debugging output since ToString() can only be called once
|
||||
/// </summary>
|
||||
private string Display
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_str != null)
|
||||
{
|
||||
return $"Decompressed: {_str}";
|
||||
}
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
if (_str != null)
|
||||
{
|
||||
// double check
|
||||
return $"Decompressed: {_str}";
|
||||
}
|
||||
|
||||
if (_bytes == null)
|
||||
{
|
||||
// This shouldn't happen
|
||||
throw new PanicException("Bytes have already been cleared");
|
||||
}
|
||||
|
||||
return $"Compressed Bytes: {_bytes.Length}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static implicit operator string(LazyCompressedString l) => l.ToString();
|
||||
|
||||
public byte[] GetBytes()
|
||||
{
|
||||
if (_bytes == null)
|
||||
{
|
||||
throw new InvalidOperationException("The bytes have already been expanded");
|
||||
}
|
||||
|
||||
return _bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the decompressed string from the bytes. This methods can only be called once.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException">Throws if this is called more than once</exception>
|
||||
public string DecompressString()
|
||||
{
|
||||
if (_str != null)
|
||||
{
|
||||
return _str;
|
||||
}
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
if (_str != null)
|
||||
{
|
||||
// double check
|
||||
return _str;
|
||||
}
|
||||
|
||||
if (_bytes == null)
|
||||
{
|
||||
throw new InvalidOperationException("Bytes have already been cleared");
|
||||
}
|
||||
|
||||
_str = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(_bytes));
|
||||
_bytes = null;
|
||||
}
|
||||
|
||||
return _str;
|
||||
}
|
||||
|
||||
public override string ToString() => DecompressString();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using MessagePack;
|
||||
using MessagePack.Formatters;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// A MessagePack formatter (deserializer) for a string key dictionary that uses <see cref="StringComparer.OrdinalIgnoreCase" /> for the key string comparison and interns the string.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
public sealed class MessagePackDictionaryStringInternIgnoreCaseFormatter<TValue> : DictionaryFormatterBase<string, TValue, Dictionary<string, TValue>, Dictionary<string, TValue>.Enumerator, Dictionary<string, TValue>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Add(Dictionary<string, TValue> collection, int index, string key, TValue value, MessagePackSerializerOptions options)
|
||||
=> collection.Add(string.Intern(key), value);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Dictionary<string, TValue> Complete(Dictionary<string, TValue> intermediateCollection)
|
||||
=> intermediateCollection;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Dictionary<string, TValue>.Enumerator GetSourceEnumerator(Dictionary<string, TValue> source)
|
||||
=> source.GetEnumerator();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Dictionary<string, TValue> Create(int count, MessagePackSerializerOptions options)
|
||||
=> new(count, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using System.Text;
|
||||
using K4os.Compression.LZ4;
|
||||
using MessagePack;
|
||||
using MessagePack.Resolvers;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes/Deserializes <see cref="ContentCacheDataModel" /> document to the SQL Database as bytes using
|
||||
/// MessagePack
|
||||
/// </summary>
|
||||
internal sealed class MsgPackContentNestedDataSerializer : IContentCacheDataSerializer
|
||||
{
|
||||
private readonly MessagePackSerializerOptions _options;
|
||||
private readonly IPropertyCacheCompression _propertyOptions;
|
||||
|
||||
public MsgPackContentNestedDataSerializer(IPropertyCacheCompression propertyOptions)
|
||||
{
|
||||
_propertyOptions = propertyOptions ?? throw new ArgumentNullException(nameof(propertyOptions));
|
||||
|
||||
MessagePackSerializerOptions? defaultOptions = ContractlessStandardResolver.Options;
|
||||
IFormatterResolver? resolver = CompositeResolver.Create(
|
||||
|
||||
// TODO: We want to be able to intern the strings for aliases when deserializing like we do for Newtonsoft but I'm unsure exactly how
|
||||
// to do that but it would seem to be with a custom message pack resolver but I haven't quite figured out based on the docs how
|
||||
// to do that since that is part of the int key -> string mapping operation, might have to see the source code to figure that one out.
|
||||
// There are docs here on how to build one of these: https://github.com/neuecc/MessagePack-CSharp/blob/master/README.md#low-level-api-imessagepackformattert
|
||||
// and there are a couple examples if you search on google for them but this will need to be a separate project.
|
||||
// NOTE: resolver custom types first
|
||||
// new ContentNestedDataResolver(),
|
||||
|
||||
// finally use standard resolver
|
||||
defaultOptions.Resolver);
|
||||
|
||||
_options = defaultOptions
|
||||
.WithResolver(resolver)
|
||||
.WithCompression(MessagePackCompression.Lz4BlockArray)
|
||||
.WithSecurity(MessagePackSecurity.UntrustedData);
|
||||
}
|
||||
|
||||
public ContentCacheDataModel? Deserialize(IReadOnlyContentBase content, string? stringData, byte[]? byteData, bool published)
|
||||
{
|
||||
if (byteData != null)
|
||||
{
|
||||
ContentCacheDataModel? cacheModel =
|
||||
MessagePackSerializer.Deserialize<ContentCacheDataModel>(byteData, _options);
|
||||
Expand(content, cacheModel, published);
|
||||
return cacheModel;
|
||||
}
|
||||
|
||||
if (stringData != null)
|
||||
{
|
||||
// NOTE: We don't really support strings but it's possible if manually used (i.e. tests)
|
||||
var bin = Convert.FromBase64String(stringData);
|
||||
ContentCacheDataModel? cacheModel = MessagePackSerializer.Deserialize<ContentCacheDataModel>(bin, _options);
|
||||
Expand(content, cacheModel, published);
|
||||
return cacheModel;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published)
|
||||
{
|
||||
Compress(content, model, published);
|
||||
var bytes = MessagePackSerializer.Serialize(model, _options);
|
||||
return new ContentCacheDataSerializationResult(null, bytes);
|
||||
}
|
||||
|
||||
public string ToJson(byte[] bin)
|
||||
{
|
||||
var json = MessagePackSerializer.ConvertToJson(bin, _options);
|
||||
return json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used during serialization to compress properties
|
||||
/// </summary>
|
||||
/// <param name="content"></param>
|
||||
/// <param name="model"></param>
|
||||
/// <param name="published"></param>
|
||||
/// <remarks>
|
||||
/// This will essentially 'double compress' property data. The MsgPack data as a whole will already be compressed
|
||||
/// but this will go a step further and double compress property data so that it is stored in the nucache file
|
||||
/// as compressed bytes and therefore will exist in memory as compressed bytes. That is, until the bytes are
|
||||
/// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant
|
||||
/// memory savings but could also affect performance of first rendering pages while decompression occurs.
|
||||
/// </remarks>
|
||||
private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model, bool published)
|
||||
{
|
||||
if (model.PropertyData is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, PropertyData[]> propertyAliasToData in model.PropertyData)
|
||||
{
|
||||
if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key, published))
|
||||
{
|
||||
foreach (PropertyData property in propertyAliasToData.Value.Where(x =>
|
||||
x.Value != null && x.Value is string))
|
||||
{
|
||||
if (property.Value is string propertyValue)
|
||||
{
|
||||
property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes(propertyValue));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (PropertyData property in propertyAliasToData.Value.Where(x =>
|
||||
x.Value != null && x.Value is int intVal))
|
||||
{
|
||||
property.Value = Convert.ToBoolean((int?)property.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used during deserialization to map the property data as lazy or expand the value
|
||||
/// </summary>
|
||||
/// <param name="content"></param>
|
||||
/// <param name="nestedData"></param>
|
||||
/// <param name="published"></param>
|
||||
private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData, bool published)
|
||||
{
|
||||
if (nestedData.PropertyData is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, PropertyData[]> propertyAliasToData in nestedData.PropertyData)
|
||||
{
|
||||
if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key, published))
|
||||
{
|
||||
foreach (PropertyData property in propertyAliasToData.Value.Where(x => x.Value != null))
|
||||
{
|
||||
if (property.Value is byte[] byteArrayValue)
|
||||
{
|
||||
property.Value = new LazyCompressedString(byteArrayValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
|
||||
internal class MsgPackContentNestedDataSerializerFactory : IContentCacheDataSerializerFactory
|
||||
{
|
||||
private readonly IPropertyCacheCompressionOptions _compressionOptions;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly ConcurrentDictionary<(int, string, bool), bool> _isCompressedCache = new();
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
|
||||
public MsgPackContentNestedDataSerializerFactory(
|
||||
IContentTypeService contentTypeService,
|
||||
IMediaTypeService mediaTypeService,
|
||||
IMemberTypeService memberTypeService,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
IPropertyCacheCompressionOptions compressionOptions)
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
_mediaTypeService = mediaTypeService;
|
||||
_memberTypeService = memberTypeService;
|
||||
_propertyEditors = propertyEditors;
|
||||
_compressionOptions = compressionOptions;
|
||||
}
|
||||
|
||||
public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types)
|
||||
{
|
||||
// Depending on which entity types are being requested, we need to look up those content types
|
||||
// to initialize the compression options.
|
||||
// We need to initialize these options now so that any data lookups required are completed and are not done while the content cache
|
||||
// is performing DB queries which will result in errors since we'll be trying to query with open readers.
|
||||
// NOTE: The calls to GetAll() below should be cached if the data has not been changed.
|
||||
var contentTypes = new Dictionary<int, IContentTypeComposition>();
|
||||
if ((types & ContentCacheDataSerializerEntityType.Document) == ContentCacheDataSerializerEntityType.Document)
|
||||
{
|
||||
foreach (IContentType ct in _contentTypeService.GetAll())
|
||||
{
|
||||
contentTypes[ct.Id] = ct;
|
||||
}
|
||||
}
|
||||
|
||||
if ((types & ContentCacheDataSerializerEntityType.Media) == ContentCacheDataSerializerEntityType.Media)
|
||||
{
|
||||
foreach (IMediaType ct in _mediaTypeService.GetAll())
|
||||
{
|
||||
contentTypes[ct.Id] = ct;
|
||||
}
|
||||
}
|
||||
|
||||
if ((types & ContentCacheDataSerializerEntityType.Member) == ContentCacheDataSerializerEntityType.Member)
|
||||
{
|
||||
foreach (IMemberType ct in _memberTypeService.GetAll())
|
||||
{
|
||||
contentTypes[ct.Id] = ct;
|
||||
}
|
||||
}
|
||||
|
||||
var compression =
|
||||
new PropertyCacheCompression(_compressionOptions, contentTypes, _propertyEditors, _isCompressedCache);
|
||||
var serializer = new MsgPackContentNestedDataSerializer(compression);
|
||||
|
||||
return serializer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
public DocumentCacheService(
|
||||
IDatabaseCacheRepository databaseCacheRepository,
|
||||
IIdKeyMap idKeyMap,
|
||||
ICoreScopeProvider scopeProvider,
|
||||
Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache,
|
||||
IPublishedContentFactory publishedContentFactory,
|
||||
ICacheNodeFactory cacheNodeFactory)
|
||||
{
|
||||
_databaseCacheRepository = databaseCacheRepository;
|
||||
_idKeyMap = idKeyMap;
|
||||
_scopeProvider = scopeProvider;
|
||||
_hybridCache = hybridCache;
|
||||
_publishedContentFactory = publishedContentFactory;
|
||||
_cacheNodeFactory = cacheNodeFactory;
|
||||
}
|
||||
|
||||
// TODO: Stop using IdKeyMap for these, but right now we both need key and id for caching..
|
||||
public async Task<IPublishedContent?> GetByKeyAsync(Guid key, bool preview = false)
|
||||
{
|
||||
Attempt<int> idAttempt = _idKeyMap.GetIdForKey(key, UmbracoObjectTypes.Document);
|
||||
if (idAttempt.Success is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
|
||||
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
|
||||
GetCacheKey(key, preview), // Unique key to the cache entry
|
||||
async cancel => await _databaseCacheRepository.GetContentSourceAsync(idAttempt.Result, preview));
|
||||
|
||||
scope.Complete();
|
||||
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public async Task SeedAsync(IReadOnlyCollection<Guid> contentTypeKeys)
|
||||
{
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
IEnumerable<ContentCacheNode> contentCacheNodes = _databaseCacheRepository.GetContentByContentTypeKey(contentTypeKeys);
|
||||
foreach (ContentCacheNode contentCacheNode in contentCacheNodes)
|
||||
{
|
||||
if (contentCacheNode.IsDraft)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Make these expiration dates configurable.
|
||||
// Never expire seeded values, we cannot do TimeSpan.MaxValue sadly, so best we can do is a year.
|
||||
var entryOptions = new HybridCacheEntryOptions
|
||||
{
|
||||
Expiration = TimeSpan.FromDays(365),
|
||||
LocalCacheExpiration = TimeSpan.FromDays(365),
|
||||
};
|
||||
|
||||
await _hybridCache.SetAsync(
|
||||
GetCacheKey(contentCacheNode.Key, false),
|
||||
contentCacheNode,
|
||||
entryOptions);
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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.
|
||||
var draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true);
|
||||
await _hybridCache.RemoveAsync(GetCacheKey(content.Key, true));
|
||||
await _databaseCacheRepository.RefreshContentAsync(draftCacheNode, content.PublishedState);
|
||||
|
||||
if (content.PublishedState == PublishedState.Publishing)
|
||||
{
|
||||
var publishedCacheNode = _cacheNodeFactory.ToContentCacheNode(content, false);
|
||||
await _hybridCache.RemoveAsync(GetCacheKey(content.Key, false));
|
||||
await _databaseCacheRepository.RefreshContentAsync(publishedCacheNode, content.PublishedState);
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
|
||||
|
||||
public async Task DeleteItemAsync(int id)
|
||||
{
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
await _databaseCacheRepository.DeleteContentItemAsync(id);
|
||||
Attempt<Guid> keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document);
|
||||
await _hybridCache.RemoveAsync(GetCacheKey(keyAttempt.Result, true));
|
||||
await _hybridCache.RemoveAsync(GetCacheKey(keyAttempt.Result, false));
|
||||
_idKeyMap.ClearCache(keyAttempt.Result);
|
||||
_idKeyMap.ClearCache(id);
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
public class DomainCacheService : IDomainCacheService
|
||||
{
|
||||
private readonly IDomainService _domainService;
|
||||
private readonly ICoreScopeProvider _coreScopeProvider;
|
||||
private readonly ConcurrentDictionary<int, Domain> _domains;
|
||||
|
||||
public DomainCacheService(IDomainService domainService, ICoreScopeProvider coreScopeProvider)
|
||||
{
|
||||
_domainService = domainService;
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
_domains = new ConcurrentDictionary<int, Domain>();
|
||||
}
|
||||
|
||||
public IEnumerable<Domain> GetAll(bool includeWildcards)
|
||||
{
|
||||
return includeWildcards == false
|
||||
? _domains.Select(x => x.Value).Where(x => x.IsWildcard == false).OrderBy(x => x.SortOrder)
|
||||
: _domains.Select(x => x.Value).OrderBy(x => x.SortOrder);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Domain> GetAssigned(int documentId, bool includeWildcards = false)
|
||||
{
|
||||
// probably this could be optimized with an index
|
||||
// but then we'd need a custom DomainStore of some sort
|
||||
IEnumerable<Domain> list = _domains.Select(x => x.Value).Where(x => x.ContentId == documentId);
|
||||
if (includeWildcards == false)
|
||||
{
|
||||
list = list.Where(x => x.IsWildcard == false);
|
||||
}
|
||||
|
||||
return list.OrderBy(x => x.SortOrder);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasAssigned(int documentId, bool includeWildcards = false)
|
||||
=> documentId > 0 && GetAssigned(documentId, includeWildcards).Any();
|
||||
|
||||
public void Refresh(DomainCacheRefresher.JsonPayload[] payloads)
|
||||
{
|
||||
foreach (DomainCacheRefresher.JsonPayload payload in payloads)
|
||||
{
|
||||
switch (payload.ChangeType)
|
||||
{
|
||||
case DomainChangeTypes.RefreshAll:
|
||||
using (ICoreScope scope = _coreScopeProvider.CreateCoreScope())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.Domains);
|
||||
LoadDomains();
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
break;
|
||||
case DomainChangeTypes.Remove:
|
||||
_domains.Remove(payload.Id, out _);
|
||||
break;
|
||||
case DomainChangeTypes.Refresh:
|
||||
IDomain? domain = _domainService.GetById(payload.Id);
|
||||
if (domain == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (domain.RootContentId.HasValue == false)
|
||||
{
|
||||
continue; // anomaly
|
||||
}
|
||||
|
||||
var culture = domain.LanguageIsoCode;
|
||||
if (string.IsNullOrWhiteSpace(culture))
|
||||
{
|
||||
continue; // anomaly
|
||||
}
|
||||
|
||||
var newDomain = new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard, domain.SortOrder);
|
||||
|
||||
// Feels wierd to use key and oldvalue, but we're using neither when updating.
|
||||
_domains.AddOrUpdate(
|
||||
domain.Id,
|
||||
new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard, domain.SortOrder),
|
||||
(key, oldValue) => newDomain);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadDomains()
|
||||
{
|
||||
IEnumerable<IDomain> domains = _domainService.GetAll(true);
|
||||
foreach (Domain domain in domains
|
||||
.Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false)
|
||||
.Select(x => new Domain(x.Id, x.DomainName, x.RootContentId!.Value, x.LanguageIsoCode!, x.IsWildcard, x.SortOrder)))
|
||||
{
|
||||
_domains.AddOrUpdate(domain.Id, domain, (key, oldValue) => domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
public interface IDocumentCacheService
|
||||
{
|
||||
Task<IPublishedContent?> GetByKeyAsync(Guid key, bool preview = false);
|
||||
|
||||
Task<IPublishedContent?> GetByIdAsync(int id, bool preview = false);
|
||||
|
||||
Task SeedAsync(IReadOnlyCollection<Guid> contentTypeKeys);
|
||||
|
||||
Task<bool> HasContentByIdAsync(int id, bool preview = false);
|
||||
|
||||
Task RefreshContentAsync(IContent content);
|
||||
|
||||
Task DeleteItemAsync(int id);
|
||||
|
||||
void Rebuild(IReadOnlyCollection<int> contentTypeKeys);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
public interface IMediaCacheService
|
||||
{
|
||||
Task<IPublishedContent?> GetByKeyAsync(Guid key);
|
||||
|
||||
Task<IPublishedContent?> GetByIdAsync(int id);
|
||||
|
||||
Task<bool> HasContentByIdAsync(int id);
|
||||
|
||||
Task RefreshMediaAsync(IMedia media);
|
||||
|
||||
Task DeleteItemAsync(int id);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
public interface IMemberCacheService
|
||||
{
|
||||
Task<IPublishedMember?> Get(IMember member);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
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;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
internal class MediaCacheService : IMediaCacheService
|
||||
{
|
||||
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;
|
||||
|
||||
public MediaCacheService(
|
||||
IDatabaseCacheRepository databaseCacheRepository,
|
||||
IIdKeyMap idKeyMap,
|
||||
ICoreScopeProvider scopeProvider,
|
||||
Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache,
|
||||
IPublishedContentFactory publishedContentFactory,
|
||||
ICacheNodeFactory cacheNodeFactory)
|
||||
{
|
||||
_databaseCacheRepository = databaseCacheRepository;
|
||||
_idKeyMap = idKeyMap;
|
||||
_scopeProvider = scopeProvider;
|
||||
_hybridCache = hybridCache;
|
||||
_publishedContentFactory = publishedContentFactory;
|
||||
_cacheNodeFactory = cacheNodeFactory;
|
||||
}
|
||||
|
||||
public async Task<IPublishedContent?> GetByKeyAsync(Guid key)
|
||||
{
|
||||
Attempt<int> idAttempt = _idKeyMap.GetIdForKey(key, UmbracoObjectTypes.Media);
|
||||
if (idAttempt.Success is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
|
||||
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
|
||||
$"{key}", // Unique key to the cache entry
|
||||
async cancel => await _databaseCacheRepository.GetMediaSourceAsync(idAttempt.Result));
|
||||
|
||||
scope.Complete();
|
||||
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedMedia(contentCacheNode);
|
||||
}
|
||||
|
||||
public async Task<IPublishedContent?> GetByIdAsync(int id)
|
||||
{
|
||||
Attempt<Guid> keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Media);
|
||||
if (keyAttempt.Success is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
|
||||
$"{keyAttempt.Result}", // Unique key to the cache entry
|
||||
async cancel => await _databaseCacheRepository.GetMediaSourceAsync(id));
|
||||
scope.Complete();
|
||||
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedMedia(contentCacheNode);
|
||||
}
|
||||
|
||||
public async Task<bool> HasContentByIdAsync(int id)
|
||||
{
|
||||
Attempt<Guid> keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Media);
|
||||
if (keyAttempt.Success is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync<ContentCacheNode?>(
|
||||
$"{keyAttempt.Result}", // Unique key to the cache entry
|
||||
cancel => ValueTask.FromResult<ContentCacheNode?>(null));
|
||||
|
||||
if (contentCacheNode is null)
|
||||
{
|
||||
await _hybridCache.RemoveAsync($"{keyAttempt.Result}");
|
||||
}
|
||||
|
||||
return contentCacheNode is not null;
|
||||
}
|
||||
|
||||
|
||||
public async Task RefreshMediaAsync(IMedia media)
|
||||
{
|
||||
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.
|
||||
var cacheNode = _cacheNodeFactory.ToContentCacheNode(media);
|
||||
await _hybridCache.SetAsync(GetCacheKey(media.Key, false), cacheNode);
|
||||
await _databaseCacheRepository.RefreshMediaAsync(cacheNode);
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
public async Task DeleteItemAsync(int id)
|
||||
{
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
await _databaseCacheRepository.DeleteContentItemAsync(id);
|
||||
Attempt<Guid> keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Media);
|
||||
if (keyAttempt.Success)
|
||||
{
|
||||
await _hybridCache.RemoveAsync(keyAttempt.Result.ToString());
|
||||
}
|
||||
|
||||
_idKeyMap.ClearCache(keyAttempt.Result);
|
||||
_idKeyMap.ClearCache(id);
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
internal class MemberCacheService : IMemberCacheService
|
||||
{
|
||||
private readonly IPublishedContentFactory _publishedContentFactory;
|
||||
|
||||
public MemberCacheService(IPublishedContentFactory publishedContentFactory) => _publishedContentFactory = publishedContentFactory;
|
||||
|
||||
public async Task<IPublishedMember?> Get(IMember member) => member is null ? null : _publishedContentFactory.ToPublishedMember(member);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>Umbraco.Cms.PublishedCache.HybridCache</PackageId>
|
||||
<Title>Umbraco CMS - Published cache - HybridCache</Title>
|
||||
<Description>Contains the published cache assembly needed to run Umbraco CMS.</Description>
|
||||
<RootNamespace>Umbraco.Cms.Infrastructure.HybridCache</RootNamespace>
|
||||
<!-- TODO: Enable package validation in v16 by removing this line -->
|
||||
<EnablePackageValidation>false</EnablePackageValidation>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" />
|
||||
<PackageReference Include="MessagePack" />
|
||||
<PackageReference Include="K4os.Compression.LZ4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.Integration</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user