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:
@@ -12,27 +12,29 @@
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.0-preview.5.24306.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.0-preview.7.24406.2" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.0-preview.5.24306.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0-preview.5.24306.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0-preview.5.24306.3" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.0-preview.7.24405.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0-preview.7.24405.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0-preview.7.24405.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.0-preview.5.24306.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.0-preview.5.24306.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="9.0.0-preview.5.24306.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.0-preview.7.24405.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0-preview.7.24405.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.SqlServer" Version="9.0.0-preview.7.24406.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0-preview.7.24405.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0-preview.7.24405.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0-preview.7.24405.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.0-preview.7.24406.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.0-preview.7.24405.7"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0-preview.7.24405.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.0-preview.7.24405.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.0-preview.7.24406.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="9.0.0-preview.7.24406.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0-preview.7.24405.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.0-preview.7.24405.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0-preview.7.24405.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.0-preview.7.24405.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.7.24406.2" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
<ItemGroup>
|
||||
@@ -83,7 +85,7 @@
|
||||
<!-- Dazinator.Extensions.FileProviders brings in a vulnerable version of System.Net.Http -->
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<!-- Examine brings in a vulnerable version of System.Security.Cryptography.Xml -->
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="9.0.0-preview.7.24405.7" />
|
||||
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring in a vulnerable version of System.Text.RegularExpressions -->
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.100-preview.5.24307.3",
|
||||
"version": "9.0.100-preview.7.24407.12",
|
||||
"rollForward": "latestFeature",
|
||||
"allowPrerelease": true
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
.AddWebServer()
|
||||
.AddRecurringBackgroundJobs()
|
||||
.AddNuCache()
|
||||
.AddUmbracoHybridCache()
|
||||
.AddDistributedCache()
|
||||
.AddCoreNotifications()
|
||||
.AddExamine()
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.PublishedCache.HybridCache\Umbraco.PublishedCache.HybridCache.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -17,8 +17,10 @@ public sealed class DomainCacheRefresher : PayloadCacheRefresherBase<DomainCache
|
||||
IPublishedSnapshotService publishedSnapshotService,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory)
|
||||
: base(appCaches, serializer, eventAggregator, factory) =>
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
{
|
||||
_publishedSnapshotService = publishedSnapshotService;
|
||||
}
|
||||
|
||||
#region Json
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ public static partial class Constants
|
||||
public const string ConfigDataTypes = ConfigPrefix + "DataTypes";
|
||||
public const string ConfigPackageManifests = ConfigPrefix + "PackageManifests";
|
||||
public const string ConfigWebhook = ConfigPrefix + "Webhook";
|
||||
public const string ConfigCache = ConfigPrefix + "Cache";
|
||||
|
||||
public static class NamedOptions
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Configuration.Models.Validation;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.DependencyInjection;
|
||||
|
||||
@@ -85,7 +85,8 @@ public static partial class UmbracoBuilderExtensions
|
||||
.AddUmbracoOptions<ContentDashboardSettings>()
|
||||
.AddUmbracoOptions<HelpPageSettings>()
|
||||
.AddUmbracoOptions<DataTypesSettings>()
|
||||
.AddUmbracoOptions<WebhookSettings>();
|
||||
.AddUmbracoOptions<WebhookSettings>()
|
||||
.AddUmbracoOptions<CacheSettings>();
|
||||
|
||||
// Configure connection string and ensure it's updated when the configuration changes
|
||||
builder.Services.AddSingleton<IConfigureOptions<ConnectionStrings>, ConfigureConnectionStrings>();
|
||||
|
||||
13
src/Umbraco.Core/Models/CacheSettings.cs
Normal file
13
src/Umbraco.Core/Models/CacheSettings.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
[UmbracoOptions(Constants.Configuration.ConfigCache)]
|
||||
public class CacheSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the collection of content type ids to always have in the cache.
|
||||
/// </summary>
|
||||
public List<Guid> ContentTypeKeys { get; set; } =
|
||||
new();
|
||||
}
|
||||
22
src/Umbraco.Core/Models/PublishedContent/IPublishedMember.cs
Normal file
22
src/Umbraco.Core/Models/PublishedContent/IPublishedMember.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
public interface IPublishedMember : IPublishedContent
|
||||
{
|
||||
public string Email { get; }
|
||||
|
||||
public string UserName { get; }
|
||||
|
||||
public string? Comments { get; }
|
||||
|
||||
public bool IsApproved { get; }
|
||||
|
||||
public bool IsLockedOut { get; }
|
||||
|
||||
public DateTime? LastLockoutDate { get; }
|
||||
|
||||
public DateTime CreationDate { get; }
|
||||
|
||||
public DateTime? LastLoginDate { get; }
|
||||
|
||||
public DateTime? LastPasswordChangedDate { get; }
|
||||
}
|
||||
@@ -10,7 +10,7 @@ public interface IPublishedMemberCache
|
||||
/// </summary>
|
||||
/// <param name="member"></param>
|
||||
/// <returns></returns>
|
||||
IPublishedContent? Get(IMember member);
|
||||
IPublishedMember? Get(IMember member);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a content type identified by its unique identifier.
|
||||
@@ -26,4 +26,12 @@ public interface IPublishedMemberCache
|
||||
/// <returns>The content type, or null.</returns>
|
||||
/// <remarks>The alias is case-insensitive.</remarks>
|
||||
IPublishedContentType GetContentType(string alias);
|
||||
|
||||
/// <summary>
|
||||
/// Get an <see cref="IPublishedContent" /> from an <see cref="IMember" />
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the member to fetch</param>
|
||||
/// <param name="preview">Will fetch draft if this is set to true</param>
|
||||
/// <returns></returns>
|
||||
Task<IPublishedMember?> GetAsync(IMember member);
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
|
||||
|
||||
var deliveryApiPropertyValueConverter = _converter as IDeliveryApiPropertyValueConverter;
|
||||
|
||||
_cacheLevel = _converter?.GetPropertyCacheLevel(this) ?? PropertyCacheLevel.Snapshot;
|
||||
_cacheLevel = _converter?.GetPropertyCacheLevel(this) ?? PropertyCacheLevel.Elements;
|
||||
_deliveryApiCacheLevel = deliveryApiPropertyValueConverter?.GetDeliveryApiPropertyCacheLevel(this) ?? _cacheLevel;
|
||||
_deliveryApiCacheLevelForExpansion = deliveryApiPropertyValueConverter?.GetDeliveryApiPropertyCacheLevelForExpansion(this) ?? _cacheLevel;
|
||||
_modelClrType = _converter?.GetPropertyValueType(this) ?? typeof(object);
|
||||
|
||||
@@ -67,7 +67,7 @@ public class ContentPickerPropertyEditor : DataEditor
|
||||
// starting in v14 the passed in value is always a guid, we store it as a document Udi string. Else it's an invalid value
|
||||
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) =>
|
||||
editorValue.Value is not null
|
||||
&& Guid.TryParse(editorValue.Value as string, out Guid guidValue)
|
||||
&& Guid.TryParse(editorValue.Value.ToString(), out Guid guidValue)
|
||||
? GuidUdi.Create(Constants.UdiEntityType.Document, guidValue).ToString()
|
||||
: null;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ public enum PropertyCacheLevel
|
||||
/// In most cases, a snapshot is created per request, and therefore this is
|
||||
/// equivalent to cache the value for the duration of the request.
|
||||
/// </remarks>
|
||||
[Obsolete("Caching no longer supports snapshotting")]
|
||||
Snapshot = 3,
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -17,14 +17,14 @@ public class ContentPickerValueConverter : PropertyValueConverterBase, IDelivery
|
||||
Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture),
|
||||
};
|
||||
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IPublishedContentCache _publishedContentCache;
|
||||
private readonly IApiContentBuilder _apiContentBuilder;
|
||||
|
||||
public ContentPickerValueConverter(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IPublishedContentCache publishedContentCache,
|
||||
IApiContentBuilder apiContentBuilder)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_publishedContentCache = publishedContentCache;
|
||||
_apiContentBuilder = apiContentBuilder;
|
||||
}
|
||||
|
||||
@@ -105,10 +105,9 @@ public class ContentPickerValueConverter : PropertyValueConverterBase, IDelivery
|
||||
PropertiesToExclude.Contains(propertyType.Alias.ToLower(CultureInfo.InvariantCulture))) == false)
|
||||
{
|
||||
IPublishedContent? content;
|
||||
IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot();
|
||||
if (inter is int id)
|
||||
{
|
||||
content = publishedSnapshot.Content?.GetById(id);
|
||||
content = _publishedContentCache.GetById(id);
|
||||
if (content != null)
|
||||
{
|
||||
return content;
|
||||
@@ -121,7 +120,7 @@ public class ContentPickerValueConverter : PropertyValueConverterBase, IDelivery
|
||||
return null;
|
||||
}
|
||||
|
||||
content = publishedSnapshot.Content?.GetById(udi.Guid);
|
||||
content = _publishedContentCache.GetById(udi.Guid);
|
||||
if (content != null && content.ContentType.ItemType == PublishedItemType.Content)
|
||||
{
|
||||
return content;
|
||||
|
||||
37
src/Umbraco.Core/PublishedCache/ICacheManager.cs
Normal file
37
src/Umbraco.Core/PublishedCache/ICacheManager.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
|
||||
namespace Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
public interface ICacheManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IPublishedContentCache" />.
|
||||
/// </summary>
|
||||
IPublishedContentCache Content { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IPublishedMediaCache" />.
|
||||
/// </summary>
|
||||
IPublishedMediaCache Media { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IPublishedMemberCache" />.
|
||||
/// </summary>
|
||||
IPublishedMemberCache Members { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IDomainCache" />.
|
||||
/// </summary>
|
||||
IDomainCache Domains { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the elements-level cache.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The elements-level cache is shared by all snapshots relying on the same elements,
|
||||
/// ie all snapshots built on top of unchanging content / media / etc.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
IAppCache ElementsCache { get; }
|
||||
}
|
||||
31
src/Umbraco.Core/PublishedCache/IDomainCacheService.cs
Normal file
31
src/Umbraco.Core/PublishedCache/IDomainCacheService.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
|
||||
namespace Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
public interface IDomainCacheService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all <see cref="Domain" /> in the current domain cache, including any domains that may be referenced by
|
||||
/// documents that are no longer published.
|
||||
/// </summary>
|
||||
/// <param name="includeWildcards"></param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<Domain> GetAll(bool includeWildcards);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all assigned <see cref="Domain" /> for specified document, even if it is not published.
|
||||
/// </summary>
|
||||
/// <param name="documentId">The document identifier.</param>
|
||||
/// <param name="includeWildcards">A value indicating whether to consider wildcard domains.</param>
|
||||
IEnumerable<Domain> GetAssigned(int documentId, bool includeWildcards = false);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a document has domains.
|
||||
/// </summary>
|
||||
/// <param name="documentId">The document identifier.</param>
|
||||
/// <param name="includeWildcards">A value indicating whether to consider wildcard domains.</param>
|
||||
bool HasAssigned(int documentId, bool includeWildcards = false);
|
||||
|
||||
void Refresh(DomainCacheRefresher.JsonPayload[] payloads);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ public interface IPublishedCache
|
||||
/// <param name="contentId">The content Udi identifier.</param>
|
||||
/// <returns>The content, or null.</returns>
|
||||
/// <remarks>The value of <paramref name="preview" /> overrides defaults.</remarks>
|
||||
[Obsolete] // FIXME: Remove when replacing nucache
|
||||
IPublishedContent? GetById(bool preview, Udi contentId);
|
||||
|
||||
/// <summary>
|
||||
@@ -56,25 +57,9 @@ public interface IPublishedCache
|
||||
/// <param name="contentId">The content unique identifier.</param>
|
||||
/// <returns>The content, or null.</returns>
|
||||
/// <remarks>Considers published or unpublished content depending on defaults.</remarks>
|
||||
[Obsolete] // FIXME: Remove when replacing nucache
|
||||
IPublishedContent? GetById(Udi contentId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the cache contains a specified content.
|
||||
/// </summary>
|
||||
/// <param name="preview">A value indicating whether to consider unpublished content.</param>
|
||||
/// <param name="contentId">The content unique identifier.</param>
|
||||
/// <returns>A value indicating whether to the cache contains the specified content.</returns>
|
||||
/// <remarks>The value of <paramref name="preview" /> overrides defaults.</remarks>
|
||||
bool HasById(bool preview, int contentId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the cache contains a specified content.
|
||||
/// </summary>
|
||||
/// <param name="contentId">The content unique identifier.</param>
|
||||
/// <returns>A value indicating whether to the cache contains the specified content.</returns>
|
||||
/// <remarks>Considers published or unpublished content depending on defaults.</remarks>
|
||||
bool HasById(int contentId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets contents at root.
|
||||
/// </summary>
|
||||
@@ -82,6 +67,7 @@ public interface IPublishedCache
|
||||
/// <param name="culture">A culture.</param>
|
||||
/// <returns>The contents.</returns>
|
||||
/// <remarks>The value of <paramref name="preview" /> overrides defaults.</remarks>
|
||||
[Obsolete] // FIXME: Remove when replacing nucache
|
||||
IEnumerable<IPublishedContent> GetAtRoot(bool preview, string? culture = null);
|
||||
|
||||
/// <summary>
|
||||
@@ -90,6 +76,7 @@ public interface IPublishedCache
|
||||
/// <param name="culture">A culture.</param>
|
||||
/// <returns>The contents.</returns>
|
||||
/// <remarks>Considers published or unpublished content depending on defaults.</remarks>
|
||||
[Obsolete] // FIXME: Remove when replacing nucache
|
||||
IEnumerable<IPublishedContent> GetAtRoot(string? culture = null);
|
||||
|
||||
/// <summary>
|
||||
@@ -98,6 +85,7 @@ public interface IPublishedCache
|
||||
/// <param name="preview">A value indicating whether to consider unpublished content.</param>
|
||||
/// <returns>A value indicating whether the cache contains published content.</returns>
|
||||
/// <remarks>The value of <paramref name="preview" /> overrides defaults.</remarks>
|
||||
[Obsolete] // FIXME: Remove when replacing nucache
|
||||
bool HasContent(bool preview);
|
||||
|
||||
/// <summary>
|
||||
@@ -105,6 +93,7 @@ public interface IPublishedCache
|
||||
/// </summary>
|
||||
/// <returns>A value indicating whether the cache contains published content.</returns>
|
||||
/// <remarks>Considers published or unpublished content depending on defaults.</remarks>
|
||||
[Obsolete] // FIXME: Remove when replacing nucache
|
||||
bool HasContent();
|
||||
|
||||
/// <summary>
|
||||
@@ -112,6 +101,7 @@ public interface IPublishedCache
|
||||
/// </summary>
|
||||
/// <param name="id">The content type unique identifier.</param>
|
||||
/// <returns>The content type, or null.</returns>
|
||||
[Obsolete("Please use the IContentTypeCacheService instead, scheduled for removal in V16")]
|
||||
IPublishedContentType? GetContentType(int id);
|
||||
|
||||
/// <summary>
|
||||
@@ -120,6 +110,7 @@ public interface IPublishedCache
|
||||
/// <param name="alias">The content type alias.</param>
|
||||
/// <returns>The content type, or null.</returns>
|
||||
/// <remarks>The alias is case-insensitive.</remarks>
|
||||
[Obsolete("Please use the IContentTypeCacheService instead, scheduled for removal in V16")]
|
||||
IPublishedContentType? GetContentType(string alias);
|
||||
|
||||
/// <summary>
|
||||
@@ -127,6 +118,7 @@ public interface IPublishedCache
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>The contents.</returns>
|
||||
[Obsolete] // FIXME: Remove when replacing nucache
|
||||
IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType);
|
||||
|
||||
/// <summary>
|
||||
@@ -134,5 +126,6 @@ public interface IPublishedCache
|
||||
/// </summary>
|
||||
/// <param name="key">The content type key.</param>
|
||||
/// <returns>The content type, or null.</returns>
|
||||
[Obsolete("Please use the IContentTypeCacheService instead, scheduled for removal in V16")]
|
||||
IPublishedContentType? GetContentType(Guid key);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,25 @@ namespace Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
public interface IPublishedContentCache : IPublishedCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a content identified by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The content unique identifier.</param>
|
||||
/// <param name="preview">A value indicating whether to consider unpublished content.</param>
|
||||
/// <returns>The content, or null.</returns>
|
||||
/// <remarks>Considers published or unpublished content depending on defaults.</remarks>
|
||||
Task<IPublishedContent?> GetByIdAsync(int id, bool preview = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a content identified by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="key">The content unique identifier.</param>
|
||||
/// <param name="preview">A value indicating whether to consider unpublished content.</param>
|
||||
/// <returns>The content, or null.</returns>
|
||||
/// <remarks>Considers published or unpublished content depending on defaults.</remarks>
|
||||
Task<IPublishedContent?> GetByIdAsync(Guid key, bool preview = false);
|
||||
|
||||
// FIXME: All these routing methods needs to be removed, as they are no longer part of the content cache
|
||||
/// <summary>
|
||||
/// Gets content identified by a route.
|
||||
/// </summary>
|
||||
@@ -24,6 +43,7 @@ public interface IPublishedContentCache : IPublishedCache
|
||||
/// </para>
|
||||
/// <para>The value of <paramref name="preview" /> overrides defaults.</para>
|
||||
/// </remarks>
|
||||
[Obsolete]
|
||||
IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null);
|
||||
|
||||
/// <summary>
|
||||
@@ -45,6 +65,7 @@ public interface IPublishedContentCache : IPublishedCache
|
||||
/// </para>
|
||||
/// <para>Considers published or unpublished content depending on defaults.</para>
|
||||
/// </remarks>
|
||||
[Obsolete]
|
||||
IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null);
|
||||
|
||||
/// <summary>
|
||||
@@ -62,6 +83,7 @@ public interface IPublishedContentCache : IPublishedCache
|
||||
/// </para>
|
||||
/// <para>The value of <paramref name="preview" /> overrides defaults.</para>
|
||||
/// </remarks>
|
||||
[Obsolete]
|
||||
string? GetRouteById(bool preview, int contentId, string? culture = null);
|
||||
|
||||
/// <summary>
|
||||
@@ -76,5 +98,6 @@ public interface IPublishedContentCache : IPublishedCache
|
||||
/// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example:
|
||||
/// {domainId}/route-path-of-item
|
||||
/// </para>
|
||||
[Obsolete]
|
||||
string? GetRouteById(int contentId, string? culture = null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
public interface IPublishedContentTypeCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Clears the entire cache.
|
||||
/// </summary>
|
||||
public void ClearAll();
|
||||
|
||||
/// <summary>
|
||||
/// Clears a cached content type.
|
||||
/// </summary>
|
||||
/// <param name="id">An identifier.</param>
|
||||
public void ClearContentType(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Clears all cached content types referencing a data type.
|
||||
/// </summary>
|
||||
/// <param name="id">A data type identifier.</param>
|
||||
public void ClearDataType(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a published content type.
|
||||
/// </summary>
|
||||
/// <param name="itemType">An item type.</param>
|
||||
/// <param name="key">An key.</param>
|
||||
/// <returns>The published content type corresponding to the item key.</returns>
|
||||
public IPublishedContentType Get(PublishedItemType itemType, Guid key);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a published content type.
|
||||
/// </summary>
|
||||
/// <param name="itemType">An item type.</param>
|
||||
/// <param name="alias">An alias.</param>
|
||||
/// <returns>The published content type corresponding to the item type and alias.</returns>
|
||||
public IPublishedContentType Get(PublishedItemType itemType, string alias);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a published content type.
|
||||
/// </summary>
|
||||
/// <param name="itemType">An item type.</param>
|
||||
/// <param name="id">An identifier.</param>
|
||||
/// <returns>The published content type corresponding to the item type and identifier.</returns>
|
||||
public IPublishedContentType Get(PublishedItemType itemType, int id);
|
||||
}
|
||||
@@ -1,5 +1,22 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
public interface IPublishedMediaCache : IPublishedCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a content identified by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The content unique identifier.</param>
|
||||
/// <returns>The content, or null.</returns>
|
||||
/// <remarks>Considers published or unpublished content depending on defaults.</remarks>
|
||||
Task<IPublishedContent?> GetByIdAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a content identified by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="key">The content unique identifier.</param>
|
||||
/// <returns>The content, or null.</returns>
|
||||
/// <remarks>Considers published or unpublished content depending on defaults.</remarks>
|
||||
Task<IPublishedContent?> GetByKeyAsync(Guid key);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ public sealed class InternalPublishedContentCache : PublishedCacheBase, IPublish
|
||||
{
|
||||
}
|
||||
|
||||
public Task<IPublishedContent?> GetByIdAsync(int id, bool preview = false) => throw new NotImplementedException();
|
||||
|
||||
public Task<IPublishedContent?> GetByIdAsync(Guid key, bool preview = false) => throw new NotImplementedException();
|
||||
|
||||
public Task<bool> HasByIdAsync(int id, bool preview = false) => 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) =>
|
||||
@@ -49,4 +55,9 @@ public sealed class InternalPublishedContentCache : PublishedCacheBase, IPublish
|
||||
|
||||
// public void Add(InternalPublishedContent content) => _content[content.Id] = content.CreateModel(Mock.Of<IPublishedModelFactory>());
|
||||
public void Clear() => _content.Clear();
|
||||
public Task<IPublishedContent?> GetByIdAsync(int id) => throw new NotImplementedException();
|
||||
|
||||
public Task<IPublishedContent?> GetByKeyAsync(Guid key) => throw new NotImplementedException();
|
||||
|
||||
public Task<bool> HasByIdAsync(int id) => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -23,13 +23,8 @@ public sealed class HtmlLocalLinkParser
|
||||
|
||||
private readonly IPublishedUrlProvider _publishedUrlProvider;
|
||||
|
||||
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
|
||||
|
||||
public HtmlLocalLinkParser(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IPublishedUrlProvider publishedUrlProvider)
|
||||
public HtmlLocalLinkParser(IPublishedUrlProvider publishedUrlProvider)
|
||||
{
|
||||
_umbracoContextAccessor = umbracoContextAccessor;
|
||||
_publishedUrlProvider = publishedUrlProvider;
|
||||
}
|
||||
|
||||
@@ -50,23 +45,7 @@ public sealed class HtmlLocalLinkParser
|
||||
/// <param name="text"></param>
|
||||
/// <param name="preview"></param>
|
||||
/// <returns></returns>
|
||||
public string EnsureInternalLinks(string text, bool preview)
|
||||
{
|
||||
if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext))
|
||||
{
|
||||
throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext");
|
||||
}
|
||||
|
||||
if (!preview)
|
||||
{
|
||||
return EnsureInternalLinks(text);
|
||||
}
|
||||
|
||||
using (umbracoContext.ForcedPreview(preview)) // force for URL provider
|
||||
{
|
||||
return EnsureInternalLinks(text);
|
||||
}
|
||||
}
|
||||
public string EnsureInternalLinks(string text, bool preview) => EnsureInternalLinks(text);
|
||||
|
||||
/// <summary>
|
||||
/// Parses the string looking for the {localLink} syntax and updates them to their correct links.
|
||||
@@ -75,11 +54,6 @@ public sealed class HtmlLocalLinkParser
|
||||
/// <returns></returns>
|
||||
public string EnsureInternalLinks(string text)
|
||||
{
|
||||
if (!_umbracoContextAccessor.TryGetUmbracoContext(out _))
|
||||
{
|
||||
throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext");
|
||||
}
|
||||
|
||||
foreach (LocalLinkTag tagData in FindLocalLinkIds(text))
|
||||
{
|
||||
if (tagData.Udi is not null)
|
||||
|
||||
@@ -31,6 +31,7 @@ public interface IUmbracoContext : IDisposable
|
||||
/// </summary>
|
||||
IPublishedSnapshot PublishedSnapshot { get; }
|
||||
|
||||
// TODO: Obsolete these, and use cache manager to get
|
||||
/// <summary>
|
||||
/// Gets the published content cache.
|
||||
/// </summary>
|
||||
|
||||
@@ -120,6 +120,7 @@ public static partial class NPocoDatabaseExtensions
|
||||
/// once T1 and T2 have completed. Whereas here, it could contain T1's value.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Obsolete("Use InsertOrUpdateAsync instead")]
|
||||
public static RecordPersistenceType InsertOrUpdate<T>(this IUmbracoDatabase db, T poco)
|
||||
where T : class =>
|
||||
db.InsertOrUpdate(poco, null, null);
|
||||
@@ -150,7 +151,7 @@ public static partial class NPocoDatabaseExtensions
|
||||
/// once T1 and T2 have completed. Whereas here, it could contain T1's value.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static RecordPersistenceType InsertOrUpdate<T>(
|
||||
public static async Task<RecordPersistenceType> InsertOrUpdateAsync<T>(
|
||||
this IUmbracoDatabase db,
|
||||
T poco,
|
||||
string? updateCommand,
|
||||
@@ -167,7 +168,7 @@ public static partial class NPocoDatabaseExtensions
|
||||
|
||||
// try to update
|
||||
var rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null
|
||||
? db.Update(poco)
|
||||
? await db.UpdateAsync(poco)
|
||||
: db.Update<T>(updateCommand!, updateArgs);
|
||||
if (rowCount > 0)
|
||||
{
|
||||
@@ -182,7 +183,7 @@ public static partial class NPocoDatabaseExtensions
|
||||
try
|
||||
{
|
||||
// try to insert
|
||||
db.Insert(poco);
|
||||
await db.InsertAsync(poco);
|
||||
return RecordPersistenceType.Insert;
|
||||
}
|
||||
catch (DbException)
|
||||
@@ -193,7 +194,7 @@ public static partial class NPocoDatabaseExtensions
|
||||
|
||||
// try to update
|
||||
rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null
|
||||
? db.Update(poco)
|
||||
? await db.UpdateAsync(poco)
|
||||
: db.Update<T>(updateCommand!, updateArgs);
|
||||
if (rowCount > 0)
|
||||
{
|
||||
@@ -209,6 +210,14 @@ public static partial class NPocoDatabaseExtensions
|
||||
throw new DataException("Record could not be inserted or updated.");
|
||||
}
|
||||
|
||||
public static RecordPersistenceType InsertOrUpdate<T>(
|
||||
this IUmbracoDatabase db,
|
||||
T poco,
|
||||
string? updateCommand,
|
||||
object? updateArgs)
|
||||
where T : class =>
|
||||
db.InsertOrUpdateAsync(poco, updateCommand, updateArgs).GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>
|
||||
/// This will escape single @ symbols for npoco values so it doesn't think it's a parameter
|
||||
/// </summary>
|
||||
|
||||
@@ -60,6 +60,21 @@ namespace Umbraco.Extensions
|
||||
return sql;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a WHERE IN clause to the Sql statement.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDto">The type of the Dto.</typeparam>
|
||||
/// <param name="sql">The Sql statement.</param>
|
||||
/// <param name="field">An expression specifying the field.</param>
|
||||
/// <param name="values">The values.</param>
|
||||
/// <returns>The Sql statement.</returns>
|
||||
public static Sql<ISqlContext> WhereIn<TDto>(this Sql<ISqlContext> sql, Expression<Func<TDto, object?>> field, IEnumerable? values, string alias)
|
||||
{
|
||||
var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(field, alias);
|
||||
sql.Where(fieldName + " IN (@values)", new { values });
|
||||
return sql;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a WHERE IN clause to the Sql statement.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.PublishedCache;
|
||||
/// Represents a content type cache.
|
||||
/// </summary>
|
||||
/// <remarks>This cache is not snapshotted, so it refreshes any time things change.</remarks>
|
||||
public class PublishedContentTypeCache : IDisposable
|
||||
public class PublishedContentTypeCache : IPublishedContentTypeCache
|
||||
{
|
||||
private readonly IContentTypeService? _contentTypeService;
|
||||
private readonly Dictionary<Guid, int> _keyToIdMap = new();
|
||||
@@ -23,11 +23,13 @@ public class PublishedContentTypeCache : IDisposable
|
||||
// NOTE: These are not concurrent dictionaries because all access is done within a lock
|
||||
private readonly Dictionary<string, IPublishedContentType> _typesByAlias = new();
|
||||
private readonly Dictionary<int, IPublishedContentType> _typesById = new();
|
||||
private bool _disposedValue;
|
||||
|
||||
// default ctor
|
||||
public PublishedContentTypeCache(IContentTypeService? contentTypeService, IMediaTypeService? mediaTypeService,
|
||||
IMemberTypeService? memberTypeService, IPublishedContentTypeFactory publishedContentTypeFactory,
|
||||
public PublishedContentTypeCache(
|
||||
IContentTypeService? contentTypeService,
|
||||
IMediaTypeService? mediaTypeService,
|
||||
IMemberTypeService? memberTypeService,
|
||||
IPublishedContentTypeFactory publishedContentTypeFactory,
|
||||
ILogger<PublishedContentTypeCache> logger)
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
@@ -48,13 +50,6 @@ public class PublishedContentTypeCache : IDisposable
|
||||
_publishedContentTypeFactory = publishedContentTypeFactory;
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(true);
|
||||
|
||||
// note: cache clearing is performed by XmlStore
|
||||
|
||||
/// <summary>
|
||||
/// Clears all cached content types.
|
||||
/// </summary>
|
||||
@@ -175,7 +170,10 @@ public class PublishedContentTypeCache : IDisposable
|
||||
|
||||
if (_keyToIdMap.TryGetValue(key, out var id))
|
||||
{
|
||||
return Get(itemType, id);
|
||||
if (_typesById.TryGetValue(id, out IPublishedContentType? foundType))
|
||||
{
|
||||
return foundType;
|
||||
}
|
||||
}
|
||||
|
||||
IPublishedContentType type = CreatePublishedContentType(itemType, key);
|
||||
@@ -289,19 +287,6 @@ public class PublishedContentTypeCache : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_lock.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetAliasKey(PublishedItemType itemType, string alias)
|
||||
{
|
||||
string k;
|
||||
|
||||
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>
|
||||
@@ -56,6 +56,12 @@ public class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigab
|
||||
public IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) =>
|
||||
GetByRoute(PreviewDefault, route, hideTopLevelNode, culture);
|
||||
|
||||
public Task<IPublishedContent?> GetByIdAsync(int id, bool preview = false) => throw new NotImplementedException();
|
||||
|
||||
public Task<IPublishedContent?> GetByIdAsync(Guid key, bool preview = false) => throw new NotImplementedException();
|
||||
|
||||
public Task<bool> HasByIdAsync(int id, bool preview = false) => throw new NotImplementedException();
|
||||
|
||||
public IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null)
|
||||
{
|
||||
if (route == null)
|
||||
|
||||
@@ -99,4 +99,10 @@ public class MediaCache : PublishedCacheBase, IPublishedMediaCache, INavigableDa
|
||||
public override IPublishedContentType? GetContentType(Guid key) => _snapshot.GetContentType(key);
|
||||
|
||||
#endregion
|
||||
|
||||
public Task<IPublishedContent?> GetByIdAsync(int id) => throw new NotImplementedException();
|
||||
|
||||
public Task<IPublishedContent?> GetByKeyAsync(Guid key) => throw new NotImplementedException();
|
||||
|
||||
public Task<bool> HasByIdAsync(int id) => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -32,10 +32,11 @@ public class MemberCache : IPublishedMemberCache, IDisposable
|
||||
public IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Member, id);
|
||||
|
||||
public IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Member, alias);
|
||||
public Task<IPublishedMember?> GetAsync(IMember member) => throw new NotImplementedException();
|
||||
|
||||
public IPublishedContent? Get(IMember member)
|
||||
public IPublishedMember? Get(IMember member)
|
||||
=>
|
||||
PublishedMember.Create(
|
||||
(IPublishedMember?)PublishedMember.Create(
|
||||
member,
|
||||
GetContentType(member.ContentTypeId),
|
||||
_previewDefault,
|
||||
@@ -58,7 +59,7 @@ public class MemberCache : IPublishedMemberCache, IDisposable
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_contentTypeCache.Dispose();
|
||||
// _contentTypeCache.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
|
||||
@@ -337,6 +337,7 @@ AND cmsContentNu.nodeId IS NULL
|
||||
Sql<ISqlContext>? sql = SqlMediaSourcesSelect(SqlContentSourcesSelectUmbracoNodeJoin)
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media))
|
||||
.Append(SqlWhereNodeIdX(SqlContext, id))
|
||||
.Append(SqlWhereNodeIdX(SqlContext, id))
|
||||
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
|
||||
|
||||
IContentCacheDataSerializer serializer =
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache;
|
||||
// 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
|
||||
internal class PublishedMember : PublishedContent, IPublishedMember
|
||||
{
|
||||
private PublishedMember(
|
||||
IMember member,
|
||||
|
||||
@@ -100,6 +100,7 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
|
||||
}
|
||||
|
||||
// Check if there is no existing content and return the no content controller
|
||||
// FIXME: This should be changed to route cache, so instead, if there are any routes, we know there is content.
|
||||
if (!umbracoContext.Content?.HasContent() ?? false)
|
||||
{
|
||||
return new RouteValueDictionary
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
<ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0-preview.5.24306.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0-preview.7.24306.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0-preview.7.24306.7" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageVersion Include="System.Data.DataSetExtensions" Version="4.5.0" />
|
||||
<PackageVersion Include="System.Data.Odbc" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0-preview.5.24306.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="System.Data.Odbc" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.0-preview.5.24306.7" />
|
||||
<PackageVersion Include="System.Data.Odbc" Version="9.0.0-preview.7.24306.7" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.0-preview.7.24306.7" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0-preview.7.24306.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0-preview.7.24306.7" />
|
||||
<PackageVersion Include="System.Data.Odbc" Version="9.0.0-preview.7.24306.7" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.0-preview.7.24306.7" />
|
||||
<PackageVersion Include="System.Reflection.Emit" Version="4.7.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
172
tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs
Normal file
172
tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Interfaces;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Common.Builders;
|
||||
|
||||
public class ContentEditingBuilder
|
||||
: BuilderBase<ContentCreateModel>,
|
||||
IWithInvariantNameBuilder,
|
||||
IWithInvariantPropertiesBuilder,
|
||||
IWithVariantsBuilder,
|
||||
IWithKeyBuilder,
|
||||
IWithContentTypeKeyBuilder,
|
||||
IWithParentKeyBuilder,
|
||||
IWithTemplateKeyBuilder
|
||||
{
|
||||
private IContentType _contentType;
|
||||
private ContentTypeBuilder _contentTypeBuilder;
|
||||
private IEnumerable<PropertyValueModel> _invariantProperties = [];
|
||||
private IEnumerable<VariantModel> _variants = [];
|
||||
private Guid _contentTypeKey;
|
||||
private Guid? _parentKey;
|
||||
private Guid? _templateKey;
|
||||
private Guid? _key;
|
||||
private string _invariantName;
|
||||
|
||||
Guid? IWithKeyBuilder.Key
|
||||
{
|
||||
get => _key;
|
||||
set => _key = value;
|
||||
}
|
||||
|
||||
string IWithInvariantNameBuilder.InvariantName
|
||||
{
|
||||
get => _invariantName;
|
||||
set => _invariantName = value;
|
||||
}
|
||||
|
||||
IEnumerable<PropertyValueModel> IWithInvariantPropertiesBuilder.InvariantProperties
|
||||
{
|
||||
get => _invariantProperties;
|
||||
set => _invariantProperties = value;
|
||||
}
|
||||
|
||||
IEnumerable<VariantModel> IWithVariantsBuilder.Variants
|
||||
{
|
||||
get => _variants;
|
||||
set => _variants = value;
|
||||
}
|
||||
|
||||
Guid? IWithParentKeyBuilder.ParentKey
|
||||
{
|
||||
get => _parentKey;
|
||||
set => _parentKey = value;
|
||||
}
|
||||
|
||||
Guid IWithContentTypeKeyBuilder.ContentTypeKey
|
||||
{
|
||||
get => _contentTypeKey;
|
||||
set => _contentTypeKey = value;
|
||||
}
|
||||
|
||||
Guid? IWithTemplateKeyBuilder.TemplateKey
|
||||
{
|
||||
get => _templateKey;
|
||||
set => _templateKey = value;
|
||||
}
|
||||
|
||||
public ContentEditingBuilder WithInvariantName(string invariantName)
|
||||
{
|
||||
_invariantName = invariantName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentEditingBuilder WithInvariantProperty(string alias, object value)
|
||||
{
|
||||
var property = new PropertyValueModel { Alias = alias, Value = value };
|
||||
_invariantProperties = _invariantProperties.Concat(new[] { property });
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentEditingBuilder AddVariant(string culture, string segment, string name,
|
||||
IEnumerable<PropertyValueModel> properties)
|
||||
{
|
||||
var variant = new VariantModel { Culture = culture, Segment = segment, Name = name, Properties = properties };
|
||||
_variants = _variants.Concat(new[] { variant });
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentEditingBuilder WithParentKey(Guid parentKey)
|
||||
{
|
||||
_parentKey = parentKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentEditingBuilder WithTemplateKey(Guid templateKey)
|
||||
{
|
||||
_templateKey = templateKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentEditingBuilder WithContentType(IContentType contentType)
|
||||
{
|
||||
_contentTypeBuilder = null;
|
||||
_contentType = contentType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override ContentCreateModel Build()
|
||||
{
|
||||
var key = _key ?? Guid.NewGuid();
|
||||
var parentKey = _parentKey;
|
||||
var templateKey = _templateKey;
|
||||
var invariantName = _invariantName ?? Guid.NewGuid().ToString();
|
||||
var invariantProperties = _invariantProperties;
|
||||
var variants = _variants;
|
||||
|
||||
if (_contentTypeBuilder is null && _contentType is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A content item cannot be constructed without providing a content type. Use AddContentType() or WithContentType().");
|
||||
}
|
||||
|
||||
var contentType = _contentType ?? _contentTypeBuilder.Build();
|
||||
var content = new ContentCreateModel();
|
||||
|
||||
content.InvariantName = invariantName;
|
||||
if (parentKey is not null)
|
||||
{
|
||||
content.ParentKey = parentKey;
|
||||
}
|
||||
|
||||
if (templateKey is not null)
|
||||
{
|
||||
content.TemplateKey = templateKey;
|
||||
}
|
||||
|
||||
content.ContentTypeKey = contentType.Key;
|
||||
content.Key = key;
|
||||
content.InvariantProperties = invariantProperties;
|
||||
content.Variants = variants;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
public static ContentCreateModel CreateBasicContent(IContentType contentType, Guid? key) =>
|
||||
new ContentEditingBuilder()
|
||||
.WithKey(key)
|
||||
.WithContentType(contentType)
|
||||
.WithInvariantName("Home")
|
||||
.Build();
|
||||
|
||||
public static ContentCreateModel CreateSimpleContent(IContentType contentType) =>
|
||||
new ContentEditingBuilder()
|
||||
.WithContentType(contentType)
|
||||
.WithInvariantName("Home")
|
||||
.WithInvariantProperty("title", "Welcome to our Home page")
|
||||
.Build();
|
||||
|
||||
public static ContentCreateModel CreateSimpleContent(IContentType contentType, string name, Guid? parentKey) =>
|
||||
new ContentEditingBuilder()
|
||||
.WithContentType(contentType)
|
||||
.WithInvariantName(name)
|
||||
.WithParentKey(parentKey)
|
||||
.WithInvariantProperty("title", "Welcome to our Home page")
|
||||
.Build();
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Interfaces;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
|
||||
public static class ContentEditingBuilderExtensions
|
||||
{
|
||||
public static T WithInvariantName<T>(this T Builder, string invariantName)
|
||||
where T : IWithInvariantNameBuilder
|
||||
{
|
||||
Builder.InvariantName = invariantName;
|
||||
return Builder;
|
||||
}
|
||||
|
||||
public static T WithInvariantProperties<T>(this T Builder, IEnumerable<PropertyValueModel> invariantProperties)
|
||||
where T : IWithInvariantPropertiesBuilder
|
||||
{
|
||||
Builder.InvariantProperties = invariantProperties;
|
||||
return Builder;
|
||||
}
|
||||
|
||||
public static T WithVariants<T>(this T Builder, IEnumerable<VariantModel> variants)
|
||||
where T : IWithVariantsBuilder
|
||||
{
|
||||
Builder.Variants = variants;
|
||||
return Builder;
|
||||
}
|
||||
|
||||
public static T WithKey<T>(this T Builder, Guid? key)
|
||||
where T : IWithKeyBuilder
|
||||
{
|
||||
Builder.Key = key;
|
||||
return Builder;
|
||||
}
|
||||
|
||||
public static T WithContentTypeKey<T>(this T Builder, Guid contentTypeKey)
|
||||
where T : IWithContentTypeKeyBuilder
|
||||
{
|
||||
Builder.ContentTypeKey = contentTypeKey;
|
||||
return Builder;
|
||||
}
|
||||
|
||||
public static T WithParentKey<T>(this T Builder, Guid? parentKey)
|
||||
where T : IWithParentKeyBuilder
|
||||
{
|
||||
Builder.ParentKey = parentKey;
|
||||
return Builder;
|
||||
}
|
||||
|
||||
|
||||
public static T WithTemplateKey<T>(this T Builder, Guid? templateKey)
|
||||
where T : IWithTemplateKeyBuilder
|
||||
{
|
||||
Builder.TemplateKey = templateKey;
|
||||
return Builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel;
|
||||
|
||||
public interface IWithContentTypeKeyBuilder
|
||||
{
|
||||
public Guid ContentTypeKey { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel;
|
||||
|
||||
public interface IWithInvariantNameBuilder
|
||||
{
|
||||
public string? InvariantName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel;
|
||||
|
||||
public interface IWithInvariantPropertiesBuilder
|
||||
{
|
||||
public IEnumerable<PropertyValueModel> InvariantProperties { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
namespace Umbraco.Cms.Tests.Common.Builders.Interfaces;
|
||||
|
||||
public interface IWithParentKeyBuilder
|
||||
{
|
||||
Guid? ParentKey { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel;
|
||||
|
||||
public interface IWithTemplateKeyBuilder
|
||||
{
|
||||
public Guid? TemplateKey { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel;
|
||||
|
||||
public interface IWithVariantsBuilder
|
||||
{
|
||||
public IEnumerable<VariantModel> Variants { get; set; }
|
||||
}
|
||||
@@ -257,6 +257,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest
|
||||
.AddUmbracoCore()
|
||||
.AddWebComponents()
|
||||
.AddNuCache()
|
||||
.AddUmbracoHybridCache()
|
||||
.AddBackOfficeCore()
|
||||
.AddBackOfficeAuthentication()
|
||||
.AddBackOfficeIdentity()
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.ContentPublishing;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrationTest
|
||||
{
|
||||
protected IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
|
||||
|
||||
protected ITemplateService TemplateService => GetRequiredService<ITemplateService>();
|
||||
|
||||
private ContentEditingService ContentEditingService =>
|
||||
(ContentEditingService)GetRequiredService<IContentEditingService>();
|
||||
|
||||
private ContentPublishingService ContentPublishingService =>
|
||||
(ContentPublishingService)GetRequiredService<IContentPublishingService>();
|
||||
|
||||
|
||||
protected ContentCreateModel Subpage2 { get; private set; }
|
||||
protected ContentCreateModel Subpage3 { get; private set; }
|
||||
|
||||
protected ContentCreateModel Subpage { get; private set; }
|
||||
|
||||
protected ContentCreateModel Textpage { get; private set; }
|
||||
|
||||
protected ContentScheduleCollection ContentSchedule { get; private set; }
|
||||
|
||||
protected CultureAndScheduleModel CultureAndSchedule { get; private set; }
|
||||
|
||||
protected int TextpageId { get; private set; }
|
||||
|
||||
protected int SubpageId { get; private set; }
|
||||
|
||||
protected int Subpage2Id { get; private set; }
|
||||
|
||||
protected int Subpage3Id { get; private set; }
|
||||
|
||||
protected ContentType ContentType { get; private set; }
|
||||
|
||||
[SetUp]
|
||||
public new void Setup() => CreateTestData();
|
||||
|
||||
protected async void CreateTestData()
|
||||
{
|
||||
// NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested.
|
||||
var template = TemplateBuilder.CreateTextPageTemplate("defaultTemplate");
|
||||
await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey);
|
||||
|
||||
// Create and Save ContentType "umbTextpage" -> 1051 (template), 1052 (content type)
|
||||
ContentType =
|
||||
ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateId: template.Id);
|
||||
ContentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522");
|
||||
ContentType.AllowedAsRoot = true;
|
||||
ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias) };
|
||||
var contentTypeResult = await ContentTypeService.CreateAsync(ContentType, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(contentTypeResult.Success);
|
||||
|
||||
// Create and Save Content "Homepage" based on "umbTextpage" -> 1053
|
||||
Textpage = ContentEditingBuilder.CreateSimpleContent(ContentType);
|
||||
Textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0");
|
||||
var createContentResultTextPage = await ContentEditingService.CreateAsync(Textpage, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(createContentResultTextPage.Success);
|
||||
|
||||
if (!Textpage.Key.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException("The content page key is null.");
|
||||
}
|
||||
|
||||
if (createContentResultTextPage.Result.Content != null)
|
||||
{
|
||||
TextpageId = createContentResultTextPage.Result.Content.Id;
|
||||
}
|
||||
|
||||
// Sets the culture and schedule for the content, in this case, we are publishing immediately for all cultures
|
||||
ContentSchedule = new ContentScheduleCollection();
|
||||
CultureAndSchedule = new CultureAndScheduleModel
|
||||
{
|
||||
CulturesToPublishImmediately = new HashSet<string> { "*" }, Schedules = ContentSchedule,
|
||||
};
|
||||
|
||||
// Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054
|
||||
Subpage = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 1", Textpage.Key);
|
||||
var createContentResultSubPage = await ContentEditingService.CreateAsync(Subpage, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(createContentResultSubPage.Success);
|
||||
|
||||
if (!Subpage.Key.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException("The content page key is null.");
|
||||
}
|
||||
|
||||
if (createContentResultSubPage.Result.Content != null)
|
||||
{
|
||||
SubpageId = createContentResultSubPage.Result.Content.Id;
|
||||
}
|
||||
|
||||
await ContentPublishingService.PublishAsync(Subpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
|
||||
// Create and Save Content "Text Page 1" based on "umbTextpage" -> 1055
|
||||
Subpage2 = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 2", Textpage.Key);
|
||||
var createContentResultSubPage2 = await ContentEditingService.CreateAsync(Subpage2, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(createContentResultSubPage2.Success);
|
||||
if (!Subpage2.Key.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException("The content page key is null.");
|
||||
}
|
||||
|
||||
if (createContentResultSubPage2.Result.Content != null)
|
||||
{
|
||||
Subpage2Id = createContentResultSubPage2.Result.Content.Id;
|
||||
}
|
||||
|
||||
Subpage3 = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 3", Textpage.Key);
|
||||
var createContentResultSubPage3 = await ContentEditingService.CreateAsync(Subpage3, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(createContentResultSubPage3.Success);
|
||||
if (!Subpage3.Key.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException("The content page key is null.");
|
||||
}
|
||||
|
||||
if (createContentResultSubPage3.Result.Content != null)
|
||||
{
|
||||
Subpage3Id = createContentResultSubPage3.Result.Content.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
|
||||
[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")]
|
||||
public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithContentEditing
|
||||
{
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
|
||||
|
||||
private IPublishedContentCache PublishedContentHybridCache => GetRequiredService<IPublishedContentCache>();
|
||||
|
||||
private IPublishedContentTypeCache PublishedContentTypeCache => GetRequiredService<IPublishedContentTypeCache>();
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Draft_Content_By_Id()
|
||||
{
|
||||
//Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
|
||||
|
||||
ContentType.RemovePropertyType("title");
|
||||
ContentTypeService.Save(ContentType);
|
||||
|
||||
// Assert
|
||||
var newTextPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
|
||||
Assert.IsNull(newTextPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Draft_Content_By_Key()
|
||||
{
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
|
||||
|
||||
ContentType.RemovePropertyType("title");
|
||||
ContentTypeService.Save(ContentType);
|
||||
//Assert
|
||||
var newTextPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
|
||||
Assert.IsNull(newTextPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Content_Gets_Removed_When_DocumentType_Is_Deleted()
|
||||
{
|
||||
// Load into cache
|
||||
var textpage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview: true);
|
||||
Assert.IsNotNull(textpage);
|
||||
|
||||
await ContentTypeService.DeleteAsync(textpage.ContentType.Key, Constants.Security.SuperUserKey);
|
||||
|
||||
var textpageAgain = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview: true);
|
||||
Assert.IsNull(textpageAgain);
|
||||
}
|
||||
|
||||
|
||||
// TODO: Copy this into PublishedContentTypeCache
|
||||
[Test]
|
||||
public async Task Can_Get_Published_DocumentType_By_Key()
|
||||
{
|
||||
var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey);
|
||||
Assert.IsNotNull(contentType);
|
||||
var contentTypeAgain = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey);
|
||||
Assert.IsNotNull(contentType);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Published_DocumentType_Gets_Deleted()
|
||||
{
|
||||
var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey);
|
||||
Assert.IsNotNull(contentType);
|
||||
|
||||
await ContentTypeService.DeleteAsync(contentType.Key, Constants.Security.SuperUserKey);
|
||||
// PublishedContentTypeCache just explodes if it doesn't exist
|
||||
Assert.Catch(() => PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentPublishing;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
|
||||
[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")]
|
||||
public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent
|
||||
{
|
||||
private IPublishedContentCache _mockedCache;
|
||||
private Mock<IDatabaseCacheRepository> _mockedNucacheRepository;
|
||||
private IDocumentCacheService _mockDocumentCacheService;
|
||||
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
|
||||
|
||||
private IContentPublishingService ContentPublishingService => GetRequiredService<IContentPublishingService>();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mockedNucacheRepository = new Mock<IDatabaseCacheRepository>();
|
||||
|
||||
var contentData = new ContentData(
|
||||
Textpage.Name,
|
||||
null,
|
||||
1,
|
||||
Textpage.UpdateDate,
|
||||
Textpage.CreatorId,
|
||||
-1,
|
||||
false,
|
||||
new Dictionary<string, PropertyData[]>(),
|
||||
null);
|
||||
_mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny<int>(), It.IsAny<bool>())).ReturnsAsync(
|
||||
new ContentCacheNode()
|
||||
{
|
||||
ContentTypeId = Textpage.ContentTypeId,
|
||||
CreatorId = Textpage.CreatorId,
|
||||
CreateDate = Textpage.CreateDate,
|
||||
Id = Textpage.Id,
|
||||
Key = Textpage.Key,
|
||||
SortOrder = 0,
|
||||
Data = contentData,
|
||||
IsDraft = true,
|
||||
});
|
||||
|
||||
_mockedNucacheRepository.Setup(r => r.GetContentByContentTypeKey(It.IsAny<IReadOnlyCollection<Guid>>())).Returns(
|
||||
new List<ContentCacheNode>()
|
||||
{
|
||||
new()
|
||||
{
|
||||
ContentTypeId = Textpage.ContentTypeId,
|
||||
CreatorId = Textpage.CreatorId,
|
||||
CreateDate = Textpage.CreateDate,
|
||||
Id = Textpage.Id,
|
||||
Key = Textpage.Key,
|
||||
SortOrder = 0,
|
||||
Data = contentData,
|
||||
IsDraft = false,
|
||||
},
|
||||
});
|
||||
|
||||
_mockedNucacheRepository.Setup(r => r.DeleteContentItemAsync(It.IsAny<int>()));
|
||||
|
||||
_mockDocumentCacheService = new DocumentCacheService(
|
||||
_mockedNucacheRepository.Object,
|
||||
GetRequiredService<IIdKeyMap>(),
|
||||
GetRequiredService<ICoreScopeProvider>(),
|
||||
GetRequiredService<Microsoft.Extensions.Caching.Hybrid.HybridCache>(),
|
||||
GetRequiredService<IPublishedContentFactory>(),
|
||||
GetRequiredService<ICacheNodeFactory>());
|
||||
|
||||
_mockedCache = new DocumentCache(_mockDocumentCacheService, GetRequiredService<IPublishedContentTypeCache>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Content_Is_Cached_By_Key()
|
||||
{
|
||||
var hybridCache = GetRequiredService<Microsoft.Extensions.Caching.Hybrid.HybridCache>();
|
||||
await hybridCache.RemoveAsync($"{Textpage.Key}+draft");
|
||||
var textPage = await _mockedCache.GetByIdAsync(Textpage.Key, true);
|
||||
var textPage2 = await _mockedCache.GetByIdAsync(Textpage.Key, true);
|
||||
AssertTextPage(textPage);
|
||||
AssertTextPage(textPage2);
|
||||
_mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny<int>(), It.IsAny<bool>()), Times.Exactly(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Content_Is_Cached_By_Id()
|
||||
{
|
||||
var hybridCache = GetRequiredService<Microsoft.Extensions.Caching.Hybrid.HybridCache>();
|
||||
await hybridCache.RemoveAsync($"{Textpage.Key}+draft");
|
||||
var textPage = await _mockedCache.GetByIdAsync(Textpage.Id, true);
|
||||
var textPage2 = await _mockedCache.GetByIdAsync(Textpage.Id, true);
|
||||
AssertTextPage(textPage);
|
||||
AssertTextPage(textPage2);
|
||||
_mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny<int>(), It.IsAny<bool>()), Times.Exactly(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Content_Is_Seeded_By_Id()
|
||||
{
|
||||
var schedule = new CultureAndScheduleModel
|
||||
{
|
||||
CulturesToPublishImmediately = new HashSet<string> { "*" }, Schedules = new ContentScheduleCollection(),
|
||||
};
|
||||
|
||||
var publishResult = await ContentPublishingService.PublishAsync(Textpage.Key, schedule, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(publishResult.Success);
|
||||
Textpage.Published = true;
|
||||
await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id);
|
||||
|
||||
await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key});
|
||||
var textPage = await _mockedCache.GetByIdAsync(Textpage.Id);
|
||||
AssertTextPage(textPage);
|
||||
|
||||
_mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny<int>(), It.IsAny<bool>()), Times.Exactly(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Content_Is_Seeded_By_Key()
|
||||
{
|
||||
var schedule = new CultureAndScheduleModel
|
||||
{
|
||||
CulturesToPublishImmediately = new HashSet<string> { "*" }, Schedules = new ContentScheduleCollection(),
|
||||
};
|
||||
|
||||
var publishResult = await ContentPublishingService.PublishAsync(Textpage.Key, schedule, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(publishResult.Success);
|
||||
Textpage.Published = true;
|
||||
await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id);
|
||||
|
||||
await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key});
|
||||
var textPage = await _mockedCache.GetByIdAsync(Textpage.Key);
|
||||
AssertTextPage(textPage);
|
||||
|
||||
_mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny<int>(), It.IsAny<bool>()), Times.Exactly(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Content_Is_Not_Seeded_If_Unpublished_By_Id()
|
||||
{
|
||||
|
||||
await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id);
|
||||
|
||||
await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key});
|
||||
var textPage = await _mockedCache.GetByIdAsync(Textpage.Id, true);
|
||||
AssertTextPage(textPage);
|
||||
|
||||
_mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny<int>(), It.IsAny<bool>()), Times.Exactly(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Content_Is_Not_Seeded_If_Unpublished_By_Key()
|
||||
{
|
||||
await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id);
|
||||
|
||||
await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key});
|
||||
var textPage = await _mockedCache.GetByIdAsync(Textpage.Key, true);
|
||||
AssertTextPage(textPage);
|
||||
|
||||
_mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny<int>(), It.IsAny<bool>()), Times.Exactly(1));
|
||||
}
|
||||
|
||||
private void AssertTextPage(IPublishedContent textPage)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsNotNull(textPage);
|
||||
Assert.AreEqual(Textpage.Name, textPage.Name);
|
||||
Assert.AreEqual(Textpage.Published, textPage.IsPublished());
|
||||
});
|
||||
AssertProperties(Textpage.Properties, textPage.Properties);
|
||||
}
|
||||
|
||||
private void AssertProperties(IPropertyCollection propertyCollection, IEnumerable<IPublishedProperty> publishedProperties)
|
||||
{
|
||||
foreach (var prop in propertyCollection)
|
||||
{
|
||||
AssertProperty(prop, publishedProperties.First(x => x.Alias == prop.Alias));
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertProperty(IProperty property, IPublishedProperty publishedProperty)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(property.Alias, publishedProperty.Alias);
|
||||
Assert.AreEqual(property.PropertyType.Alias, publishedProperty.PropertyType.Alias);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.ContentPublishing;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
|
||||
[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")]
|
||||
public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest
|
||||
{
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
|
||||
|
||||
private ICacheManager CacheManager => GetRequiredService<ICacheManager>();
|
||||
|
||||
private ITemplateService TemplateService => GetRequiredService<ITemplateService>();
|
||||
|
||||
private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
|
||||
|
||||
private IContentEditingService ContentEditingService => GetRequiredService<IContentEditingService>();
|
||||
|
||||
private IContentPublishingService ContentPublishingService => GetRequiredService<IContentPublishingService>();
|
||||
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Value_From_ContentPicker()
|
||||
{
|
||||
var template = TemplateBuilder.CreateTextPageTemplate();
|
||||
await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey);
|
||||
var textPage = await CreateTextPageDocument(template.Id);
|
||||
var contentPickerDocument = await CreateContentPickerDocument(template.Id, textPage.Key);
|
||||
|
||||
var contentPickerPage = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id);
|
||||
|
||||
IPublishedContent contentPickerValue = (IPublishedContent)contentPickerPage.Value("contentPicker");
|
||||
Assert.AreEqual(textPage.Key, contentPickerValue.Key);
|
||||
Assert.AreEqual(textPage.Id, contentPickerValue.Id);
|
||||
Assert.AreEqual(textPage.Name, contentPickerValue.Name);
|
||||
Assert.AreEqual("The title value", contentPickerValue.Properties.First(x => x.Alias == "title").GetValue());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Value_From_Updated_ContentPicker()
|
||||
{
|
||||
var template = TemplateBuilder.CreateTextPageTemplate();
|
||||
await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey);
|
||||
var textPage = await CreateTextPageDocument(template.Id);
|
||||
var contentPickerDocument = await CreateContentPickerDocument(template.Id, textPage.Key);
|
||||
|
||||
// Get for caching
|
||||
var notUpdatedContent = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id);
|
||||
|
||||
IPublishedContent contentPickerValue = (IPublishedContent)notUpdatedContent.Value("contentPicker");
|
||||
Assert.AreEqual("The title value", contentPickerValue.Properties.First(x => x.Alias == "title").GetValue());
|
||||
|
||||
// Update content
|
||||
var updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantName = "Root Create",
|
||||
InvariantProperties = new[]
|
||||
{
|
||||
new PropertyValueModel { Alias = "title", Value = "Updated title" },
|
||||
new PropertyValueModel { Alias = "bodyText", Value = "The body text" }
|
||||
},
|
||||
};
|
||||
|
||||
var updateResult = await ContentEditingService.UpdateAsync(textPage.Key, updateModel, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(updateResult.Success);
|
||||
|
||||
var publishResult = await ContentPublishingService.PublishAsync(
|
||||
updateResult.Result.Content!.Key,
|
||||
new CultureAndScheduleModel()
|
||||
{
|
||||
CulturesToPublishImmediately = new HashSet<string> {"*"},
|
||||
Schedules = new ContentScheduleCollection(),
|
||||
},
|
||||
Constants.Security.SuperUserKey);
|
||||
|
||||
Assert.IsTrue(publishResult);
|
||||
|
||||
var contentPickerPage = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id);
|
||||
IPublishedContent updatedPickerValue = (IPublishedContent)contentPickerPage.Value("contentPicker");
|
||||
|
||||
|
||||
Assert.AreEqual(textPage.Key, updatedPickerValue.Key);
|
||||
Assert.AreEqual(textPage.Id, updatedPickerValue.Id);
|
||||
Assert.AreEqual(textPage.Name, updatedPickerValue.Name);
|
||||
Assert.AreEqual("Updated title", updatedPickerValue.Properties.First(x => x.Alias == "title").GetValue());
|
||||
}
|
||||
|
||||
private async Task<IContent> CreateContentPickerDocument(int templateId, Guid textPageKey)
|
||||
{
|
||||
var builder = new ContentTypeBuilder();
|
||||
var pickerContentType = (ContentType)builder
|
||||
.WithAlias("test")
|
||||
.WithName("TestName")
|
||||
.AddAllowedTemplate()
|
||||
.WithId(templateId)
|
||||
.Done()
|
||||
.AddPropertyGroup()
|
||||
.WithName("Content")
|
||||
.WithSupportsPublishing(true)
|
||||
.AddPropertyType()
|
||||
.WithAlias("contentPicker")
|
||||
.WithName("Content Picker")
|
||||
.WithDataTypeId(1046)
|
||||
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.ContentPicker)
|
||||
.WithValueStorageType(ValueStorageType.Integer)
|
||||
.WithSortOrder(16)
|
||||
.Done()
|
||||
.Done()
|
||||
.Build();
|
||||
|
||||
pickerContentType.AllowedAsRoot = true;
|
||||
ContentTypeService.Save(pickerContentType);
|
||||
|
||||
|
||||
var createOtherModel = new ContentCreateModel
|
||||
{
|
||||
ContentTypeKey = pickerContentType.Key,
|
||||
ParentKey = Constants.System.RootKey,
|
||||
InvariantName = "Test Create",
|
||||
InvariantProperties = new[] { new PropertyValueModel { Alias = "contentPicker", Value = textPageKey }, },
|
||||
};
|
||||
|
||||
var result = await ContentEditingService.CreateAsync(createOtherModel, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status);
|
||||
|
||||
var publishResult = await ContentPublishingService.PublishAsync(
|
||||
result.Result.Content!.Key,
|
||||
new CultureAndScheduleModel()
|
||||
{
|
||||
CulturesToPublishImmediately = new HashSet<string> {"*"},
|
||||
Schedules = new ContentScheduleCollection(),
|
||||
},
|
||||
Constants.Security.SuperUserKey);
|
||||
|
||||
return result.Result.Content;
|
||||
}
|
||||
|
||||
private async Task<IContent> CreateTextPageDocument(int templateId)
|
||||
{
|
||||
var textContentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: templateId);
|
||||
textContentType.AllowedAsRoot = true;
|
||||
ContentTypeService.Save(textContentType);
|
||||
|
||||
var createModel = new ContentCreateModel
|
||||
{
|
||||
ContentTypeKey = textContentType.Key,
|
||||
ParentKey = Constants.System.RootKey,
|
||||
InvariantName = "Root Create",
|
||||
InvariantProperties = new[]
|
||||
{
|
||||
new PropertyValueModel { Alias = "title", Value = "The title value" },
|
||||
new PropertyValueModel { Alias = "bodyText", Value = "The body text" }
|
||||
},
|
||||
};
|
||||
|
||||
var createResult = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(createResult.Success);
|
||||
|
||||
var publishResult = await ContentPublishingService.PublishAsync(
|
||||
createResult.Result.Content!.Key,
|
||||
new CultureAndScheduleModel()
|
||||
{
|
||||
CulturesToPublishImmediately = new HashSet<string> {"*"},
|
||||
Schedules = new ContentScheduleCollection(),
|
||||
},
|
||||
Constants.Security.SuperUserKey);
|
||||
|
||||
Assert.IsTrue(publishResult.Success);
|
||||
return createResult.Result.Content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
|
||||
[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")]
|
||||
public class DocumentHybridCacheScopeTests : UmbracoIntegrationTestWithContentEditing
|
||||
{
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
|
||||
|
||||
private IPublishedContentCache PublishedContentHybridCache => GetRequiredService<IPublishedContentCache>();
|
||||
|
||||
private IContentPublishingService ContentPublishingService => GetRequiredService<IContentPublishingService>();
|
||||
|
||||
private ICoreScopeProvider ICoreScopeProvider => GetRequiredService<ICoreScopeProvider>();
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Correct_Content_After_Rollback_With_Id()
|
||||
{
|
||||
using (ICoreScopeProvider.CreateCoreScope())
|
||||
{
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
}
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId);
|
||||
|
||||
// Published page should not be in cache, as we rolled scope back.
|
||||
Assert.IsNull(textPage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Correct_Content_After_Rollback_With_Key()
|
||||
{
|
||||
using (ICoreScopeProvider.CreateCoreScope())
|
||||
{
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
}
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value);
|
||||
|
||||
// Published page should not be in cache, as we rolled scope back.
|
||||
Assert.IsNull(textPage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Document_After_Scope_Complete_With_Id()
|
||||
{
|
||||
using (var scope = ICoreScopeProvider.CreateCoreScope())
|
||||
{
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
// Act
|
||||
var publishedPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId);
|
||||
|
||||
// Published page should not be in cache, as we rolled scope back.
|
||||
Assert.IsNotNull(publishedPage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Document_After_Scope_Completes_With_Key()
|
||||
{
|
||||
using (var scope = ICoreScopeProvider.CreateCoreScope())
|
||||
{
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
// Act
|
||||
var publishedPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value);
|
||||
|
||||
// Published page should not be in cache, as we rolled scope back.
|
||||
Assert.IsNotNull(publishedPage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
|
||||
[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")]
|
||||
public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
|
||||
{
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
|
||||
|
||||
private IPublishedContentCache PublishedContentHybridCache => GetRequiredService<IPublishedContentCache>();
|
||||
|
||||
private IContentEditingService ContentEditingService => GetRequiredService<IContentEditingService>();
|
||||
|
||||
private IContentPublishingService ContentPublishingService => GetRequiredService<IContentPublishingService>();
|
||||
|
||||
private const string NewName = "New Name";
|
||||
private const string NewTitle = "New Title";
|
||||
|
||||
|
||||
// Create CRUD Tests for Content, Also cultures.
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Draft_Content_By_Id()
|
||||
{
|
||||
//Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
|
||||
|
||||
//Assert
|
||||
AssertTextPage(textPage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Draft_Content_By_Key()
|
||||
{
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
|
||||
|
||||
// Assert
|
||||
AssertTextPage(textPage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Published_Content_By_Id()
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId);
|
||||
|
||||
// Assert
|
||||
AssertTextPage(textPage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Published_Content_By_Key()
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value);
|
||||
|
||||
// Assert
|
||||
AssertTextPage(textPage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Draft_Of_Published_Content_By_Id()
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
|
||||
|
||||
// Assert
|
||||
AssertTextPage(textPage);
|
||||
Assert.IsFalse(textPage.IsPublished());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Draft_Of_Published_Content_By_Key()
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
|
||||
|
||||
// Assert
|
||||
AssertTextPage(textPage);
|
||||
Assert.IsFalse(textPage.IsPublished());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Updated_Draft_Content_By_Id()
|
||||
{
|
||||
// Arrange
|
||||
Textpage.InvariantName = NewName;
|
||||
ContentUpdateModel updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantName = NewName,
|
||||
InvariantProperties = Textpage.InvariantProperties,
|
||||
Variants = Textpage.Variants,
|
||||
TemplateKey = Textpage.TemplateKey,
|
||||
};
|
||||
await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var updatedPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(NewName, updatedPage.Name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Updated_Draft_Content_By_Key()
|
||||
{
|
||||
// Arrange
|
||||
Textpage.InvariantName = NewName;
|
||||
ContentUpdateModel updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantName = NewName,
|
||||
InvariantProperties = Textpage.InvariantProperties,
|
||||
Variants = Textpage.Variants,
|
||||
TemplateKey = Textpage.TemplateKey,
|
||||
};
|
||||
await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var updatedPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(NewName, updatedPage.Name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(true, true)]
|
||||
[TestCase(false, false)]
|
||||
// BETTER NAMING, CURRENTLY THIS IS TESTING BOTH THE PUBLISHED AND THE DRAFT OF THE PUBLISHED.
|
||||
public async Task Can_Get_Updated_Draft_Published_Content_By_Id(bool preview, bool result)
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
Textpage.InvariantName = NewName;
|
||||
ContentUpdateModel updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantName = NewName,
|
||||
InvariantProperties = Textpage.InvariantProperties,
|
||||
Variants = Textpage.Variants,
|
||||
TemplateKey = Textpage.TemplateKey,
|
||||
};
|
||||
await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(result, NewName.Equals(textPage.Name));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(true, true)]
|
||||
[TestCase(false, false)]
|
||||
// BETTER NAMING, CURRENTLY THIS IS TESTING BOTH THE PUBLISHED AND THE DRAFT OF THE PUBLISHED.
|
||||
public async Task Can_Get_Updated_Draft_Published_Content_By_Key(bool preview, bool result)
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
|
||||
Textpage.InvariantName = NewName;
|
||||
|
||||
ContentUpdateModel updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantName = NewName,
|
||||
InvariantProperties = Textpage.InvariantProperties,
|
||||
Variants = Textpage.Variants,
|
||||
TemplateKey = Textpage.TemplateKey,
|
||||
};
|
||||
|
||||
await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(result, NewName.Equals(textPage.Name));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Draft_Content_Property_By_Id()
|
||||
{
|
||||
// Arrange
|
||||
var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value;
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(titleValue, textPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Draft_Content_Property_By_Key()
|
||||
{
|
||||
// Arrange
|
||||
var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value;
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(titleValue, textPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Published_Content_Property_By_Id()
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value;
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(titleValue, textPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Published_Content_Property_By_Key()
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value;
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(titleValue, textPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Draft_Of_Published_Content_Property_By_Id()
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value;
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(titleValue, textPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Draft_Of_Published_Content_Property_By_Key()
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value;
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(titleValue, textPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Updated_Draft_Content_Property_By_Id()
|
||||
{
|
||||
// Arrange
|
||||
Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle;
|
||||
|
||||
ContentUpdateModel updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantName = Textpage.InvariantName,
|
||||
InvariantProperties = Textpage.InvariantProperties,
|
||||
Variants = Textpage.Variants,
|
||||
TemplateKey = Textpage.TemplateKey,
|
||||
};
|
||||
|
||||
await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(NewTitle, textPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Updated_Draft_Content_Property_By_Key()
|
||||
{
|
||||
// Arrange
|
||||
Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle;
|
||||
|
||||
ContentUpdateModel updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantName = Textpage.InvariantName,
|
||||
InvariantProperties = Textpage.InvariantProperties,
|
||||
Variants = Textpage.Variants,
|
||||
TemplateKey = Textpage.TemplateKey,
|
||||
};
|
||||
|
||||
await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(NewTitle, textPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Updated_Published_Content_Property_By_Id()
|
||||
{
|
||||
// Arrange
|
||||
Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle;
|
||||
|
||||
ContentUpdateModel updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantName = Textpage.InvariantName,
|
||||
InvariantProperties = Textpage.InvariantProperties,
|
||||
Variants = Textpage.Variants,
|
||||
TemplateKey = Textpage.TemplateKey,
|
||||
};
|
||||
|
||||
await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(NewTitle, textPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Updated_Published_Content_Property_By_Key()
|
||||
{
|
||||
// Arrange
|
||||
Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle;
|
||||
|
||||
ContentUpdateModel updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantName = Textpage.InvariantName,
|
||||
InvariantProperties = Textpage.InvariantProperties,
|
||||
Variants = Textpage.Variants,
|
||||
TemplateKey = Textpage.TemplateKey,
|
||||
};
|
||||
|
||||
await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(NewTitle, textPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(true, "New Title")]
|
||||
[TestCase(false, "Welcome to our Home page")]
|
||||
public async Task Can_Get_Updated_Draft_Of_Published_Content_Property_By_Id(bool preview, string titleName)
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle;
|
||||
|
||||
ContentUpdateModel updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantName = Textpage.InvariantName,
|
||||
InvariantProperties = Textpage.InvariantProperties,
|
||||
Variants = Textpage.Variants,
|
||||
TemplateKey = Textpage.TemplateKey,
|
||||
};
|
||||
|
||||
await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(titleName, textPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(true, "New Name")]
|
||||
[TestCase(false, "Welcome to our Home page")]
|
||||
public async Task Can_Get_Updated_Draft_Of_Published_Content_Property_By_Key(bool preview, string titleName)
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
Textpage.InvariantProperties.First(x => x.Alias == "title").Value = titleName;
|
||||
|
||||
ContentUpdateModel updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantName = Textpage.InvariantName,
|
||||
InvariantProperties = Textpage.InvariantProperties,
|
||||
Variants = Textpage.Variants,
|
||||
TemplateKey = Textpage.TemplateKey,
|
||||
};
|
||||
|
||||
await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(titleName, textPage.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Not_Get_Deleted_Content_By_Id()
|
||||
{
|
||||
// Arrange
|
||||
var content = await PublishedContentHybridCache.GetByIdAsync(Subpage3Id, true);
|
||||
Assert.IsNotNull(content);
|
||||
await ContentEditingService.DeleteAsync(Subpage3.Key.Value, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage3Id, true);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(textPage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Not_Get_Deleted_Content_By_Key()
|
||||
{
|
||||
// Arrange
|
||||
await PublishedContentHybridCache.GetByIdAsync(Subpage3.Key.Value, true);
|
||||
var result = await ContentEditingService.DeleteAsync(Subpage3.Key.Value, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage3.Key.Value, true);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(textPage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Can_Not_Get_Deleted_Published_Content_By_Id(bool preview)
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
await ContentEditingService.DeleteAsync(Textpage.Key.Value, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(textPage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Can_Not_Get_Deleted_Published_Content_By_Key(bool preview)
|
||||
{
|
||||
// Arrange
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
|
||||
await ContentEditingService.DeleteAsync(Textpage.Key.Value, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(textPage);
|
||||
}
|
||||
|
||||
private void AssertTextPage(IPublishedContent textPage)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsNotNull(textPage);
|
||||
Assert.AreEqual(Textpage.Key, textPage.Key);
|
||||
Assert.AreEqual(Textpage.ContentTypeKey, textPage.ContentType.Key);
|
||||
Assert.AreEqual(Textpage.InvariantName, textPage.Name);
|
||||
});
|
||||
|
||||
AssertProperties(Textpage.InvariantProperties, textPage.Properties);
|
||||
}
|
||||
|
||||
private void AssertProperties(IEnumerable<PropertyValueModel> propertyCollection, IEnumerable<IPublishedProperty> publishedProperties)
|
||||
{
|
||||
foreach (var prop in propertyCollection)
|
||||
{
|
||||
AssertProperty(prop, publishedProperties.First(x => x.Alias == prop.Alias));
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertProperty(PropertyValueModel property, IPublishedProperty publishedProperty)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(property.Alias, publishedProperty.Alias);
|
||||
Assert.AreEqual(property.Value, publishedProperty.GetSourceValue());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
|
||||
[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")]
|
||||
public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest
|
||||
{
|
||||
private string _englishIsoCode = "en-US";
|
||||
private string _danishIsoCode = "da-DK";
|
||||
private string _variantTitleAlias = "variantTitle";
|
||||
private string _variantTitleName = "Variant Title";
|
||||
private string _invariantTitleAlias = "invariantTitle";
|
||||
private string _invariantTitleName = "Invariant Title";
|
||||
|
||||
private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
|
||||
|
||||
private ILanguageService LanguageService => GetRequiredService<ILanguageService>();
|
||||
|
||||
private IContentEditingService ContentEditingService => GetRequiredService<IContentEditingService>();
|
||||
|
||||
private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService<IUmbracoContextFactory>();
|
||||
|
||||
private IPublishedContentCache PublishedContentHybridCache => GetRequiredService<IPublishedContentCache>();
|
||||
|
||||
private IContent VariantPage { get; set; }
|
||||
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
|
||||
|
||||
[SetUp]
|
||||
public async Task Setup() => await CreateTestData();
|
||||
|
||||
[Test]
|
||||
public async Task Can_Set_Invariant_Title()
|
||||
{
|
||||
// Arrange
|
||||
await PublishedContentHybridCache.GetByIdAsync(VariantPage.Id, true);
|
||||
var updatedInvariantTitle = "Updated Invariant Title";
|
||||
var updatedVariantTitle = "Updated Variant Title";
|
||||
|
||||
|
||||
var updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantProperties = new[]
|
||||
{
|
||||
new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle }
|
||||
},
|
||||
Variants = new []
|
||||
{
|
||||
new VariantModel
|
||||
{
|
||||
Culture = _englishIsoCode,
|
||||
Name = "Updated English Name",
|
||||
Properties = new []
|
||||
{
|
||||
new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle }
|
||||
}
|
||||
},
|
||||
new VariantModel
|
||||
{
|
||||
Culture = _danishIsoCode,
|
||||
Name = "Updated Danish Name",
|
||||
Properties = new []
|
||||
{
|
||||
new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle }
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var result = await ContentEditingService.UpdateAsync(VariantPage.Key, updateModel, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(VariantPage.Id, true);
|
||||
|
||||
// Assert
|
||||
using var contextReference = UmbracoContextFactory.EnsureUmbracoContext();
|
||||
Assert.AreEqual(updatedInvariantTitle, textPage.Value(_invariantTitleAlias, "", ""));
|
||||
Assert.AreEqual(updatedVariantTitle, textPage.Value(_variantTitleAlias, _englishIsoCode));
|
||||
Assert.AreEqual(updatedVariantTitle, textPage.Value(_variantTitleAlias, _danishIsoCode));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Set_Invariant_Title_On_One_Culture()
|
||||
{
|
||||
// Arrange
|
||||
await PublishedContentHybridCache.GetByIdAsync(VariantPage.Id, true);
|
||||
var updatedInvariantTitle = "Updated Invariant Title";
|
||||
var updatedVariantTitle = "Updated Invariant Title";
|
||||
|
||||
|
||||
var updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantProperties = new[]
|
||||
{
|
||||
new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle }
|
||||
},
|
||||
Variants = new []
|
||||
{
|
||||
new VariantModel
|
||||
{
|
||||
Culture = _englishIsoCode,
|
||||
Name = "Updated English Name",
|
||||
Properties = new []
|
||||
{
|
||||
new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle }
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var result = await ContentEditingService.UpdateAsync(VariantPage.Key, updateModel, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
|
||||
// Act
|
||||
var textPage = await PublishedContentHybridCache.GetByIdAsync(VariantPage.Id, true);
|
||||
|
||||
// Assert
|
||||
using var contextReference = UmbracoContextFactory.EnsureUmbracoContext();
|
||||
Assert.AreEqual(updatedInvariantTitle, textPage.Value(_invariantTitleAlias, "", ""));
|
||||
Assert.AreEqual(updatedVariantTitle, textPage.Value(_variantTitleAlias, _englishIsoCode));
|
||||
Assert.AreEqual(_variantTitleName, textPage.Value(_variantTitleAlias, _danishIsoCode));
|
||||
}
|
||||
|
||||
|
||||
private async Task CreateTestData()
|
||||
{
|
||||
// NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested.
|
||||
var language = new LanguageBuilder()
|
||||
.WithCultureInfo(_danishIsoCode)
|
||||
.Build();
|
||||
|
||||
await LanguageService.CreateAsync(language, Constants.Security.SuperUserKey);
|
||||
|
||||
var contentType = new ContentTypeBuilder()
|
||||
.WithAlias("cultureVariationTest")
|
||||
.WithName("Culture Variation Test")
|
||||
.WithContentVariation(ContentVariation.Culture)
|
||||
.AddPropertyType()
|
||||
.WithAlias(_variantTitleAlias)
|
||||
.WithName(_variantTitleName)
|
||||
.WithVariations(ContentVariation.Culture)
|
||||
.Done()
|
||||
.AddPropertyType()
|
||||
.WithAlias(_invariantTitleAlias)
|
||||
.WithName(_invariantTitleName)
|
||||
.WithVariations(ContentVariation.Nothing)
|
||||
.Done()
|
||||
.Build();
|
||||
contentType.AllowedAsRoot = true;
|
||||
ContentTypeService.Save(contentType);
|
||||
var rootContentCreateModel = new ContentCreateModel
|
||||
{
|
||||
ContentTypeKey = contentType.Key,
|
||||
Variants = new[]
|
||||
{
|
||||
new VariantModel
|
||||
{
|
||||
Culture = "en-US",
|
||||
Name = "English Page",
|
||||
Properties = new []
|
||||
{
|
||||
new PropertyValueModel { Alias = _variantTitleAlias, Value = _variantTitleName }
|
||||
},
|
||||
},
|
||||
new VariantModel
|
||||
{
|
||||
Culture = "da-DK",
|
||||
Name = "Danish Page",
|
||||
Properties = new []
|
||||
{
|
||||
new PropertyValueModel { Alias = _variantTitleAlias, Value = _variantTitleName }
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var result = await ContentEditingService.CreateAsync(rootContentCreateModel, Constants.Security.SuperUserKey);
|
||||
VariantPage = result.Result.Content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
|
||||
[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")]
|
||||
public class MediaHybridCacheTests : UmbracoIntegrationTest
|
||||
{
|
||||
private IPublishedMediaCache PublishedMediaHybridCache => GetRequiredService<IPublishedMediaCache>();
|
||||
|
||||
private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService<IUmbracoContextFactory>();
|
||||
|
||||
private IMediaTypeService MediaTypeService => GetRequiredService<IMediaTypeService>();
|
||||
|
||||
private IMediaEditingService MediaEditingService => GetRequiredService<IMediaEditingService>();
|
||||
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
|
||||
|
||||
// TODO: make test with MediaWithCrops
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Media_By_Key()
|
||||
{
|
||||
// Arrange
|
||||
var newMediaType = new MediaTypeBuilder()
|
||||
.WithAlias("album")
|
||||
.WithName("Album")
|
||||
.Build();
|
||||
|
||||
newMediaType.AllowedAsRoot = true;
|
||||
MediaTypeService.Save(newMediaType);
|
||||
|
||||
var createModel = new MediaCreateModel
|
||||
{
|
||||
ContentTypeKey = newMediaType.Key,
|
||||
ParentKey = Constants.System.RootKey,
|
||||
InvariantName = "Image",
|
||||
};
|
||||
|
||||
var result = await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
|
||||
// Act
|
||||
var media = await PublishedMediaHybridCache.GetByKeyAsync(result.Result.Content.Key);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(media);
|
||||
Assert.AreEqual("Image", media.Name);
|
||||
Assert.AreEqual(newMediaType.Key, media.ContentType.Key);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Media_By_Id()
|
||||
{
|
||||
// Arrange
|
||||
var newMediaType = new MediaTypeBuilder()
|
||||
.WithAlias("album")
|
||||
.WithName("Album")
|
||||
.Build();
|
||||
|
||||
newMediaType.AllowedAsRoot = true;
|
||||
MediaTypeService.Save(newMediaType);
|
||||
|
||||
var createModel = new MediaCreateModel
|
||||
{
|
||||
ContentTypeKey = newMediaType.Key,
|
||||
ParentKey = Constants.System.RootKey,
|
||||
InvariantName = "Image",
|
||||
};
|
||||
|
||||
var result = await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
|
||||
// Act
|
||||
var media = await PublishedMediaHybridCache.GetByIdAsync(result.Result.Content.Id);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(media);
|
||||
Assert.AreEqual("Image", media.Name);
|
||||
Assert.AreEqual(newMediaType.Key, media.ContentType.Key);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_Get_Non_Existing_Media_By_Key()
|
||||
{
|
||||
// Act
|
||||
var media = await PublishedMediaHybridCache.GetByKeyAsync(Guid.NewGuid());
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(media);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_Get_Non_Existing_Media_By_Id()
|
||||
{
|
||||
// Act
|
||||
var media = await PublishedMediaHybridCache.GetByIdAsync(124214);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(media);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Media_Property_By_Key()
|
||||
{
|
||||
// Arrange
|
||||
var media = await CreateMedia();
|
||||
|
||||
// Act
|
||||
var publishedMedia = await PublishedMediaHybridCache.GetByKeyAsync(media.Key);
|
||||
|
||||
UmbracoContextFactory.EnsureUmbracoContext();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(media);
|
||||
Assert.AreEqual("Image", media.Name);
|
||||
Assert.AreEqual("NewTitle", publishedMedia.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Media_Property_By_Id()
|
||||
{
|
||||
// Arrange
|
||||
var media = await CreateMedia();
|
||||
|
||||
// Act
|
||||
var publishedMedia = await PublishedMediaHybridCache.GetByKeyAsync(media.Key);
|
||||
|
||||
UmbracoContextFactory.EnsureUmbracoContext();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(publishedMedia);
|
||||
Assert.AreEqual("Image", publishedMedia.Name);
|
||||
Assert.AreEqual("NewTitle", publishedMedia.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Updated_Media()
|
||||
{
|
||||
// Arrange
|
||||
var media = await CreateMedia();
|
||||
await PublishedMediaHybridCache.GetByIdAsync(media.Id);
|
||||
|
||||
// Act
|
||||
var updateModel = new MediaUpdateModel()
|
||||
{
|
||||
InvariantName = "Update name",
|
||||
InvariantProperties = new List<PropertyValueModel>()
|
||||
{
|
||||
new()
|
||||
{
|
||||
Alias = "title",
|
||||
Value = "Updated Title"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var updateAttempt = await MediaEditingService.UpdateAsync(media.Key, updateModel, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(updateAttempt.Success);
|
||||
var publishedMedia = await PublishedMediaHybridCache.GetByIdAsync(media.Id);
|
||||
UmbracoContextFactory.EnsureUmbracoContext();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(publishedMedia);
|
||||
Assert.AreEqual("Update name", publishedMedia.Name);
|
||||
Assert.AreEqual("Updated Title", publishedMedia.Value("title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_Get_Deleted_Media_By_Id()
|
||||
{
|
||||
// Arrange
|
||||
var media = await CreateMedia();
|
||||
var publishedMedia = await PublishedMediaHybridCache.GetByIdAsync(media.Id);
|
||||
Assert.IsNotNull(publishedMedia);
|
||||
|
||||
await MediaEditingService.DeleteAsync(media.Key, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var deletedMedia = await PublishedMediaHybridCache.GetByIdAsync(media.Id);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(deletedMedia);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_Get_Deleted_Media_By_Key()
|
||||
{
|
||||
// Arrange
|
||||
var media = await CreateMedia();
|
||||
var publishedMedia = await PublishedMediaHybridCache.GetByKeyAsync(media.Key);
|
||||
Assert.IsNotNull(publishedMedia);
|
||||
|
||||
await MediaEditingService.DeleteAsync(media.Key, Constants.Security.SuperUserKey);
|
||||
|
||||
// Act
|
||||
var deletedMedia = await PublishedMediaHybridCache.GetByKeyAsync(media.Key);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(deletedMedia);
|
||||
}
|
||||
|
||||
private async Task<IMedia> CreateMedia()
|
||||
{
|
||||
IMediaType mediaType = MediaTypeBuilder.CreateSimpleMediaType("test", "Test");
|
||||
mediaType.AllowedAsRoot = true;
|
||||
MediaTypeService.Save(mediaType);
|
||||
|
||||
var createModel = new MediaCreateModel
|
||||
{
|
||||
ContentTypeKey = mediaType.Key,
|
||||
ParentKey = Constants.System.RootKey,
|
||||
InvariantName = "Image",
|
||||
InvariantProperties = new List<PropertyValueModel>()
|
||||
{
|
||||
new()
|
||||
{
|
||||
Alias = "title",
|
||||
Value = "NewTitle"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
return result.Result.Content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
|
||||
[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")]
|
||||
public class MemberHybridCacheTests : UmbracoIntegrationTest
|
||||
{
|
||||
private IPublishedMemberCache PublishedMemberHybridCache => GetRequiredService<IPublishedMemberCache>();
|
||||
|
||||
private IMemberEditingService MemberEditingService => GetRequiredService<IMemberEditingService>();
|
||||
|
||||
private IMemberService MemberService => GetRequiredService<IMemberService>();
|
||||
|
||||
private IMemberTypeService MemberTypeService => GetRequiredService<IMemberTypeService>();
|
||||
|
||||
private IMemberGroupService MemberGroupService => GetRequiredService<IMemberGroupService>();
|
||||
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
|
||||
|
||||
[Test]
|
||||
public async Task Can_Get_Member_By_Key()
|
||||
{
|
||||
Guid key = Guid.NewGuid();
|
||||
var createdMember = await CreateMemberAsync(key);
|
||||
|
||||
// Act
|
||||
var member = await PublishedMemberHybridCache.GetAsync(createdMember);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(member);
|
||||
Assert.AreEqual("The title value", member.Value("title"));
|
||||
Assert.AreEqual("test@test.com", member.Email);
|
||||
Assert.AreEqual("test", member.UserName);
|
||||
Assert.IsTrue(member.IsApproved);
|
||||
Assert.AreEqual("T. Est", member.Name);
|
||||
}
|
||||
|
||||
private async Task<IMember> CreateMemberAsync(Guid? key = null, bool titleIsSensitive = false)
|
||||
{
|
||||
IMemberType memberType = MemberTypeBuilder.CreateSimpleMemberType();
|
||||
memberType.SetIsSensitiveProperty("title", titleIsSensitive);
|
||||
MemberTypeService.Save(memberType);
|
||||
MemberService.AddRole("RoleOne");
|
||||
var group = MemberGroupService.GetByName("RoleOne");
|
||||
|
||||
var createModel = new MemberCreateModel
|
||||
{
|
||||
Key = key,
|
||||
Email = "test@test.com",
|
||||
Username = "test",
|
||||
Password = "SuperSecret123",
|
||||
IsApproved = true,
|
||||
ContentTypeKey = memberType.Key,
|
||||
Roles = new [] { group.Key },
|
||||
InvariantName = "T. Est",
|
||||
InvariantProperties = new[]
|
||||
{
|
||||
new PropertyValueModel { Alias = "title", Value = "The title value" },
|
||||
new PropertyValueModel { Alias = "author", Value = "The author value" }
|
||||
}
|
||||
};
|
||||
|
||||
var result = await MemberEditingService.CreateAsync(createModel, SuperUser());
|
||||
Assert.IsTrue(result.Success);
|
||||
return result.Result.Content;
|
||||
}
|
||||
|
||||
private IUser SuperUser() => GetRequiredService<IUserService>().GetAsync(Constants.Security.SuperUserKey).GetAwaiter().GetResult();
|
||||
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
<ProjectReference Include="..\..\src\Umbraco.Cms.Api.Management\Umbraco.Cms.Api.Management.csproj" />
|
||||
<ProjectReference Include="..\..\src\Umbraco.Cms.Persistence.EFCore\Umbraco.Cms.Persistence.EFCore.csproj" />
|
||||
<ProjectReference Include="..\..\src\Umbraco.Cms\Umbraco.Cms.csproj" />
|
||||
<ProjectReference Include="..\..\src\Umbraco.PublishedCache.HybridCache\Umbraco.PublishedCache.HybridCache.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Tests.Common\Umbraco.Tests.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.UrlAndDomains;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Mapper = true, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)]
|
||||
[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")]
|
||||
public class DomainAndUrlsTests : UmbracoIntegrationTest
|
||||
{
|
||||
[SetUp]
|
||||
@@ -66,6 +67,7 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddUnique<IVariationContextAccessor>(_variationContextAccessor);
|
||||
builder.AddUmbracoHybridCache();
|
||||
builder.AddNuCache();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public class ContentPickerValueConverterTests : PropertyValueConverterTests
|
||||
{
|
||||
private ContentPickerValueConverter CreateValueConverter(IApiContentNameProvider? nameProvider = null)
|
||||
=> new ContentPickerValueConverter(
|
||||
PublishedSnapshotAccessor,
|
||||
PublishedContentCacheMock.Object,
|
||||
new ApiContentBuilder(
|
||||
nameProvider ?? new ApiContentNameProvider(),
|
||||
CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()),
|
||||
|
||||
@@ -10,7 +10,6 @@ using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Templates;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
|
||||
|
||||
@@ -24,7 +23,7 @@ public class MarkdownEditorValueConverterTests : PropertyValueConverterTests
|
||||
[TestCase(123, "")]
|
||||
public void MarkdownEditorValueConverter_ConvertsValueToMarkdownString(object inter, string expected)
|
||||
{
|
||||
var linkParser = new HtmlLocalLinkParser(Mock.Of<IUmbracoContextAccessor>(), Mock.Of<IPublishedUrlProvider>());
|
||||
var linkParser = new HtmlLocalLinkParser(Mock.Of<IPublishedUrlProvider>());
|
||||
var urlParser = new HtmlUrlParser(Mock.Of<IOptionsMonitor<ContentSettings>>(), Mock.Of<ILogger<HtmlUrlParser>>(), Mock.Of<IProfilingLogger>(), Mock.Of<IIOHelper>());
|
||||
var valueConverter = new MarkdownEditorValueConverter(linkParser, urlParser);
|
||||
|
||||
|
||||
@@ -375,7 +375,7 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe
|
||||
|
||||
internal PublishedElementPropertyBase CreateContentPickerProperty(IPublishedElement parent, Guid pickedContentKey, string propertyTypeAlias, IApiContentBuilder contentBuilder)
|
||||
{
|
||||
ContentPickerValueConverter contentPickerValueConverter = new ContentPickerValueConverter(PublishedSnapshotAccessor, contentBuilder);
|
||||
ContentPickerValueConverter contentPickerValueConverter = new ContentPickerValueConverter(PublishedContentCacheMock.Object, contentBuilder);
|
||||
var contentPickerPropertyType = SetupPublishedPropertyType(contentPickerValueConverter, propertyTypeAlias, Constants.PropertyEditors.Aliases.ContentPicker);
|
||||
|
||||
return new PublishedElementPropertyBase(contentPickerPropertyType, parent, false, PropertyCacheLevel.None, new GuidUdi(Constants.UdiEntityType.Document, pickedContentKey).ToString());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user