diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs
index ad5b326035..3641510fd6 100644
--- a/src/Umbraco.Core/Constants-SqlTemplates.cs
+++ b/src/Umbraco.Core/Constants-SqlTemplates.cs
@@ -30,6 +30,7 @@ public static partial class Constants
public static class NuCacheDatabaseDataSource
{
public const string WhereNodeId = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeId";
+ public const string WhereNodeKey = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeKey";
public const string WhereNodeIdX = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeIdX";
public const string SourcesSelectUmbracoNodeJoin =
diff --git a/src/Umbraco.Core/Factories/NavigationFactory.cs b/src/Umbraco.Core/Factories/NavigationFactory.cs
index 316c6031d6..815312e048 100644
--- a/src/Umbraco.Core/Factories/NavigationFactory.cs
+++ b/src/Umbraco.Core/Factories/NavigationFactory.cs
@@ -9,11 +9,10 @@ internal static class NavigationFactory
///
/// Builds a dictionary of NavigationNode objects from a given dataset.
///
+ /// A dictionary of objects with key corresponding to their unique Guid.
/// The objects used to build the navigation nodes dictionary.
- /// A dictionary of objects with key corresponding to their unique Guid.
- public static ConcurrentDictionary BuildNavigationDictionary(IEnumerable entities)
+ public static void BuildNavigationDictionary(ConcurrentDictionary nodesStructure,IEnumerable entities)
{
- var nodesStructure = new ConcurrentDictionary();
var entityList = entities.ToList();
var idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key);
@@ -39,7 +38,5 @@ internal static class NavigationFactory
parentNode.AddChild(node);
}
}
-
- return nodesStructure;
}
}
diff --git a/src/Umbraco.Core/Models/CacheSettings.cs b/src/Umbraco.Core/Models/CacheSettings.cs
index dcd7211347..2d4373a4da 100644
--- a/src/Umbraco.Core/Models/CacheSettings.cs
+++ b/src/Umbraco.Core/Models/CacheSettings.cs
@@ -1,13 +1,29 @@
-using Umbraco.Cms.Core.Configuration.Models;
+using System.ComponentModel;
+using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Core.Models;
[UmbracoOptions(Constants.Configuration.ConfigCache)]
public class CacheSettings
{
+ internal const int StaticDocumentBreadthFirstSeedCount = 100;
+
+ internal const int StaticMediaBreadthFirstSeedCount = 100;
+ internal const string StaticSeedCacheDuration = "365.00:00:00";
+
///
/// Gets or sets a value for the collection of content type ids to always have in the cache.
///
public List ContentTypeKeys { get; set; } =
new();
+
+ [DefaultValue(StaticDocumentBreadthFirstSeedCount)]
+ public int DocumentBreadthFirstSeedCount { get; set; } = StaticDocumentBreadthFirstSeedCount;
+
+
+ [DefaultValue(StaticMediaBreadthFirstSeedCount)]
+ public int MediaBreadthFirstSeedCount { get; set; } = StaticDocumentBreadthFirstSeedCount;
+
+ [DefaultValue(StaticSeedCacheDuration)]
+ public TimeSpan SeedCacheDuration { get; set; } = TimeSpan.Parse(StaticSeedCacheDuration);
}
diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs
index e5755c8d87..394223c311 100644
--- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs
+++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs
@@ -36,6 +36,9 @@ internal abstract class ContentNavigationServiceBase
public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys)
=> TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys);
+ public bool TryGetRootKeys(out IEnumerable childrenKeys)
+ => TryGetRootKeysFromStructure(_navigationStructure, out childrenKeys);
+
public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys)
=> TryGetDescendantsKeysFromStructure(_navigationStructure, parentKey, out descendantsKeys);
@@ -162,6 +165,7 @@ internal abstract class ContentNavigationServiceBase
_recycleBinNavigationStructure.TryRemove(key, out _);
}
+
///
/// Rebuilds the navigation structure based on the specified object type key and whether the items are trashed.
/// Only relevant for items in the content and media trees (which have readLock values of -333 or -334).
@@ -184,7 +188,7 @@ internal abstract class ContentNavigationServiceBase
_navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey) :
_navigationRepository.GetContentNodesByObjectType(objectTypeKey);
- _navigationStructure = NavigationFactory.BuildNavigationDictionary(navigationModels);
+ NavigationFactory.BuildNavigationDictionary(_navigationStructure, navigationModels);
}
private bool TryGetParentKeyFromStructure(ConcurrentDictionary structure, Guid childKey, out Guid? parentKey)
@@ -213,6 +217,13 @@ internal abstract class ContentNavigationServiceBase
return true;
}
+ private bool TryGetRootKeysFromStructure(ConcurrentDictionary structure, out IEnumerable childrenKeys)
+ {
+ // TODO can we make this more efficient?
+ childrenKeys = structure.Values.Where(x=>x.Parent is null).Select(x=>x.Key);
+ return true;
+ }
+
private bool TryGetDescendantsKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable descendantsKeys)
{
var descendants = new List();
diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs
index 4e28f80bb6..9b6fb9807d 100644
--- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs
+++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs
@@ -9,6 +9,7 @@ public interface INavigationQueryService
bool TryGetParentKey(Guid childKey, out Guid? parentKey);
bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys);
+ bool TryGetRootKeys(out IEnumerable childrenKeys);
bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys);
diff --git a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs
index 6ad695c154..984fcbe110 100644
--- a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -11,6 +11,8 @@ 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.SeedKeyProviders.Document;
+using Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Media;
using Umbraco.Cms.Infrastructure.HybridCache.Serialization;
using Umbraco.Cms.Infrastructure.HybridCache.Services;
@@ -62,6 +64,17 @@ public static class UmbracoBuilderExtensions
builder.AddNotificationAsyncHandler();
builder.AddNotificationAsyncHandler();
builder.AddNotificationAsyncHandler();
+ builder.AddCacheSeeding();
+ return builder;
+ }
+
+ private static IUmbracoBuilder AddCacheSeeding(this IUmbracoBuilder builder)
+ {
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+
+
+ builder.Services.AddSingleton();
return builder;
}
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs
index 7fd91c4603..accc962c5c 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs
@@ -17,7 +17,7 @@ internal class CacheNodeFactory : ICacheNodeFactory
public ContentCacheNode ToContentCacheNode(IContent content, bool preview)
{
- ContentData contentData = GetContentData(content, !preview, preview ? content.PublishTemplateId : content.TemplateId);
+ ContentData contentData = GetContentData(content, !preview, preview ? content.TemplateId : content.PublishTemplateId);
return new ContentCacheNode
{
Id = content.Id,
diff --git a/src/Umbraco.PublishedCache.HybridCache/IDocumentSeedKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/IDocumentSeedKeyProvider.cs
new file mode 100644
index 0000000000..9fa39fd072
--- /dev/null
+++ b/src/Umbraco.PublishedCache.HybridCache/IDocumentSeedKeyProvider.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Infrastructure.HybridCache;
+
+public interface IDocumentSeedKeyProvider : ISeedKeyProvider
+{
+
+}
diff --git a/src/Umbraco.PublishedCache.HybridCache/IMediaSeedKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/IMediaSeedKeyProvider.cs
new file mode 100644
index 0000000000..54ec4926fd
--- /dev/null
+++ b/src/Umbraco.PublishedCache.HybridCache/IMediaSeedKeyProvider.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Infrastructure.HybridCache;
+
+public interface IMediaSeedKeyProvider : ISeedKeyProvider
+{
+
+}
diff --git a/src/Umbraco.PublishedCache.HybridCache/ISeedKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/ISeedKeyProvider.cs
new file mode 100644
index 0000000000..5883c89dc3
--- /dev/null
+++ b/src/Umbraco.PublishedCache.HybridCache/ISeedKeyProvider.cs
@@ -0,0 +1,10 @@
+namespace Umbraco.Cms.Infrastructure.HybridCache;
+
+public interface ISeedKeyProvider
+{
+ ///
+ /// Gets keys of documents that should be seeded into the cache.
+ ///
+ /// Keys to seed
+ ISet GetSeedKeys();
+}
diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs
index 105fad1d9d..a38c0408a1 100644
--- a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs
@@ -51,7 +51,7 @@ internal sealed class CacheRefreshingNotificationHandler :
foreach (IContent deletedEntity in notification.DeletedEntities)
{
await RefreshElementsCacheAsync(deletedEntity);
- await _documentCacheService.DeleteItemAsync(deletedEntity.Id);
+ await _documentCacheService.DeleteItemAsync(deletedEntity);
}
}
@@ -66,7 +66,7 @@ internal sealed class CacheRefreshingNotificationHandler :
foreach (IMedia deletedEntity in notification.DeletedEntities)
{
await RefreshElementsCacheAsync(deletedEntity);
- await _mediaCacheService.DeleteItemAsync(deletedEntity.Id);
+ await _mediaCacheService.DeleteItemAsync(deletedEntity);
}
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs
index d0dfa76b67..1cea1a2360 100644
--- a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs
@@ -1,7 +1,7 @@
-using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Events;
-using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.HybridCache.Services;
namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers;
@@ -9,13 +9,28 @@ namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers;
internal class SeedingNotificationHandler : INotificationAsyncHandler
{
private readonly IDocumentCacheService _documentCacheService;
- private readonly CacheSettings _cacheSettings;
+ private readonly IMediaCacheService _mediaCacheService;
+ private readonly IRuntimeState _runtimeState;
- public SeedingNotificationHandler(IDocumentCacheService documentCacheService, IOptions cacheSettings)
+ public SeedingNotificationHandler(IDocumentCacheService documentCacheService, IMediaCacheService mediaCacheService, IRuntimeState runtimeState)
{
_documentCacheService = documentCacheService;
- _cacheSettings = cacheSettings.Value;
+ _mediaCacheService = mediaCacheService;
+ _runtimeState = runtimeState;
}
- public async Task HandleAsync(UmbracoApplicationStartedNotification notification, CancellationToken cancellationToken) => await _documentCacheService.SeedAsync(_cacheSettings.ContentTypeKeys);
+ public async Task HandleAsync(UmbracoApplicationStartedNotification notification,
+ CancellationToken cancellationToken)
+ {
+
+ if (_runtimeState.Level <= RuntimeLevel.Install)
+ {
+ return;
+ }
+
+ await Task.WhenAll(
+ _documentCacheService.SeedAsync(cancellationToken),
+ _mediaCacheService.SeedAsync(cancellationToken)
+ );
+ }
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs
index d49d2f8799..0f917508aa 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs
@@ -65,8 +65,13 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
{
IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
- // always refresh the edited data
- await OnRepositoryRefreshed(serializer, contentCacheNode, true);
+ // We always cache draft and published separately, so we only want to cache drafts if the node is a draft type.
+ if (contentCacheNode.IsDraft)
+ {
+ await OnRepositoryRefreshed(serializer, contentCacheNode, true);
+ // if it's a draft node we don't need to worry about the published state
+ return;
+ }
switch (publishedState)
{
@@ -208,11 +213,36 @@ AND cmsContentNu.nodeId IS NULL
return CreateContentNodeKit(dto, serializer, preview);
}
- public IEnumerable GetContentByContentTypeKey(IEnumerable keys)
+ public async Task GetContentSourceAsync(Guid key, bool preview = false)
{
+ Sql? sql = SqlContentSourcesSelect()
+ .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document))
+ .Append(SqlWhereNodeKey(SqlContext, key))
+ .Append(SqlOrderByLevelIdSortOrder(SqlContext));
+
+ ContentSourceDto? dto = await Database.FirstOrDefaultAsync(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);
+ }
+
+ private IEnumerable GetContentSourceByDocumentTypeKey(IEnumerable documentTypeKeys)
+ {
+ Guid[] keys = documentTypeKeys.ToArray();
if (keys.Any() is false)
{
- yield break;
+ return [];
}
Sql? sql = SqlContentSourcesSelect()
@@ -222,17 +252,26 @@ AND cmsContentNu.nodeId IS NULL
.WhereIn(x => x.UniqueId, keys,"n")
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
+ return GetContentNodeDtos(sql);
+ }
+
+ public IEnumerable GetContentByContentTypeKey(IEnumerable keys)
+ {
+ IEnumerable dtos = GetContentSourceByDocumentTypeKey(keys);
+
IContentCacheDataSerializer serializer =
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
- IEnumerable dtos = GetContentNodeDtos(sql);
-
foreach (ContentSourceDto row in dtos)
{
yield return CreateContentNodeKit(row, serializer, row.Published is false);
}
}
+ ///
+ public IEnumerable GetContentKeysByContentTypeKeys(IEnumerable keys, bool published = false)
+ => GetContentSourceByDocumentTypeKey(keys).Where(x => x.Published == published).Select(x => x.Key);
+
public async Task GetMediaSourceAsync(int id)
{
Sql? sql = SqlMediaSourcesSelect()
@@ -252,6 +291,25 @@ AND cmsContentNu.nodeId IS NULL
return CreateMediaNodeKit(dto, serializer);
}
+ public async Task GetMediaSourceAsync(Guid key)
+ {
+ Sql? sql = SqlMediaSourcesSelect()
+ .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media))
+ .Append(SqlWhereNodeKey(SqlContext, key))
+ .Append(SqlOrderByLevelIdSortOrder(SqlContext));
+
+ ContentSourceDto? dto = await Database.FirstOrDefaultAsync(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
@@ -642,6 +700,19 @@ WHERE cmsContentNu.nodeId IN (
return sql;
}
+ private Sql SqlWhereNodeKey(ISqlContext sqlContext, Guid key)
+ {
+ ISqlSyntaxProvider syntax = sqlContext.SqlSyntax;
+
+ SqlTemplate sqlTemplate = sqlContext.Templates.Get(
+ Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeKey,
+ builder =>
+ builder.Where(x => x.UniqueId == SqlTemplate.Arg("key")));
+
+ Sql sql = sqlTemplate.Sql(key);
+ return sql;
+ }
+
private Sql SqlOrderByLevelIdSortOrder(ISqlContext sqlContext)
{
ISqlSyntaxProvider syntax = sqlContext.SqlSyntax;
diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs
index 47c18c07e1..6a88d5405e 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs
@@ -8,10 +8,22 @@ internal interface IDatabaseCacheRepository
Task GetContentSourceAsync(int id, bool preview = false);
+ Task GetContentSourceAsync(Guid key, bool preview = false);
+
Task GetMediaSourceAsync(int id);
+ Task GetMediaSourceAsync(Guid key);
+
+
IEnumerable GetContentByContentTypeKey(IEnumerable keys);
+ ///
+ /// Gets all content keys of specific document types
+ ///
+ /// The document types to find content using.
+ /// The keys of all content use specific document types.
+ IEnumerable GetContentKeysByContentTypeKeys(IEnumerable keys, bool published = false);
+
///
/// Refreshes the nucache database row for the given cache node />
///
diff --git a/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/BreadthFirstKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/BreadthFirstKeyProvider.cs
new file mode 100644
index 0000000000..99a5fe50a3
--- /dev/null
+++ b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/BreadthFirstKeyProvider.cs
@@ -0,0 +1,67 @@
+using Umbraco.Cms.Core.Services.Navigation;
+
+namespace Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders;
+
+public abstract class BreadthFirstKeyProvider
+{
+ private readonly INavigationQueryService _navigationQueryService;
+ private readonly int _seedCount;
+
+ public BreadthFirstKeyProvider(INavigationQueryService navigationQueryService, int seedCount)
+ {
+ _navigationQueryService = navigationQueryService;
+ _seedCount = seedCount;
+ }
+
+ public ISet GetSeedKeys()
+ {
+ if (_seedCount == 0)
+ {
+ return new HashSet();
+ }
+
+ Queue keyQueue = new();
+ HashSet keys = [];
+ int keyCount = 0;
+
+ if (_navigationQueryService.TryGetRootKeys(out IEnumerable rootKeys) is false)
+ {
+ return new HashSet();
+ }
+
+ foreach (Guid key in rootKeys)
+ {
+ keyCount++;
+ keys.Add(key);
+ keyQueue.Enqueue(key);
+ if (keyCount == _seedCount)
+ {
+ return keys;
+ }
+ }
+
+ while (keyQueue.Count > 0 && keyCount < _seedCount)
+ {
+ Guid key = keyQueue.Dequeue();
+
+ if (_navigationQueryService.TryGetChildrenKeys(key, out IEnumerable childKeys) is false)
+ {
+ continue;
+ }
+
+ foreach (Guid childKey in childKeys)
+ {
+ keys.Add(childKey);
+ keyCount++;
+ if (keyCount == _seedCount)
+ {
+ return keys;
+ }
+
+ keyQueue.Enqueue(childKey);
+ }
+ }
+
+ return keys;
+ }
+}
diff --git a/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/ContentTypeSeedKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/ContentTypeSeedKeyProvider.cs
new file mode 100644
index 0000000000..bb0d721d63
--- /dev/null
+++ b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/ContentTypeSeedKeyProvider.cs
@@ -0,0 +1,32 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Scoping;
+using Umbraco.Cms.Infrastructure.HybridCache.Persistence;
+
+namespace Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document;
+
+internal sealed class ContentTypeSeedKeyProvider : IDocumentSeedKeyProvider
+{
+ private readonly ICoreScopeProvider _scopeProvider;
+ private readonly IDatabaseCacheRepository _databaseCacheRepository;
+ private readonly CacheSettings _cacheSettings;
+
+ public ContentTypeSeedKeyProvider(
+ ICoreScopeProvider scopeProvider,
+ IDatabaseCacheRepository databaseCacheRepository,
+ IOptions cacheSettings)
+ {
+ _scopeProvider = scopeProvider;
+ _databaseCacheRepository = databaseCacheRepository;
+ _cacheSettings = cacheSettings.Value;
+ }
+
+ public ISet GetSeedKeys()
+ {
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+ var documentKeys = _databaseCacheRepository.GetContentKeysByContentTypeKeys(_cacheSettings.ContentTypeKeys, published: true).ToHashSet();
+ scope.Complete();
+
+ return documentKeys;
+ }
+}
diff --git a/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/DocumentBreadthFirstKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/DocumentBreadthFirstKeyProvider.cs
new file mode 100644
index 0000000000..1e991d6277
--- /dev/null
+++ b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/DocumentBreadthFirstKeyProvider.cs
@@ -0,0 +1,15 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Services.Navigation;
+
+namespace Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document;
+
+internal sealed class DocumentBreadthFirstKeyProvider : BreadthFirstKeyProvider, IDocumentSeedKeyProvider
+{
+ public DocumentBreadthFirstKeyProvider(
+ IDocumentNavigationQueryService documentNavigationQueryService,
+ IOptions cacheSettings)
+ : base(documentNavigationQueryService, cacheSettings.Value.DocumentBreadthFirstSeedCount)
+ {
+ }
+}
diff --git a/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Media/MediaBreadthFirstKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Media/MediaBreadthFirstKeyProvider.cs
new file mode 100644
index 0000000000..657ec4b8a0
--- /dev/null
+++ b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Media/MediaBreadthFirstKeyProvider.cs
@@ -0,0 +1,14 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Services.Navigation;
+
+namespace Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Media;
+
+internal sealed class MediaBreadthFirstKeyProvider : BreadthFirstKeyProvider, IMediaSeedKeyProvider
+{
+ public MediaBreadthFirstKeyProvider(
+ IMediaNavigationQueryService navigationQueryService, IOptions cacheSettings)
+ : base(navigationQueryService, cacheSettings.Value.MediaBreadthFirstSeedCount)
+ {
+ }
+}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs
index b0aa936793..b91ea182f2 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs
@@ -1,4 +1,7 @@
-using Microsoft.Extensions.Caching.Hybrid;
+using System.Diagnostics;
+using Microsoft.Extensions.Caching.Hybrid;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
@@ -6,6 +9,7 @@ using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
using Umbraco.Cms.Infrastructure.HybridCache.Persistence;
+using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
@@ -17,7 +21,30 @@ internal sealed class DocumentCacheService : IDocumentCacheService
private readonly Microsoft.Extensions.Caching.Hybrid.HybridCache _hybridCache;
private readonly IPublishedContentFactory _publishedContentFactory;
private readonly ICacheNodeFactory _cacheNodeFactory;
+ private readonly IEnumerable _seedKeyProviders;
+ private readonly IPublishedModelFactory _publishedModelFactory;
+ private readonly CacheSettings _cacheSettings;
+ private HashSet? _seedKeys;
+ private HashSet SeedKeys
+ {
+ get
+ {
+ if (_seedKeys is not null)
+ {
+ return _seedKeys;
+ }
+
+ _seedKeys = [];
+
+ foreach (IDocumentSeedKeyProvider provider in _seedKeyProviders)
+ {
+ _seedKeys.UnionWith(provider.GetSeedKeys());
+ }
+
+ return _seedKeys;
+ }
+ }
public DocumentCacheService(
IDatabaseCacheRepository databaseCacheRepository,
@@ -25,7 +52,10 @@ internal sealed class DocumentCacheService : IDocumentCacheService
ICoreScopeProvider scopeProvider,
Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache,
IPublishedContentFactory publishedContentFactory,
- ICacheNodeFactory cacheNodeFactory)
+ ICacheNodeFactory cacheNodeFactory,
+ IEnumerable seedKeyProviders,
+ IOptions cacheSettings,
+ IPublishedModelFactory publishedModelFactory)
{
_databaseCacheRepository = databaseCacheRepository;
_idKeyMap = idKeyMap;
@@ -33,25 +63,21 @@ internal sealed class DocumentCacheService : IDocumentCacheService
_hybridCache = hybridCache;
_publishedContentFactory = publishedContentFactory;
_cacheNodeFactory = cacheNodeFactory;
+ _seedKeyProviders = seedKeyProviders;
+ _publishedModelFactory = publishedModelFactory;
+ _cacheSettings = cacheSettings.Value;
}
- // TODO: Stop using IdKeyMap for these, but right now we both need key and id for caching..
public async Task GetByKeyAsync(Guid key, bool preview = false)
{
- Attempt 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));
+ async cancel => await _databaseCacheRepository.GetContentSourceAsync(key, preview));
scope.Complete();
- return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview);
+ return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory);
}
public async Task GetByIdAsync(int id, bool preview = false)
@@ -67,37 +93,55 @@ internal sealed class DocumentCacheService : IDocumentCacheService
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);
+ return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory);;
}
- public async Task SeedAsync(IReadOnlyCollection contentTypeKeys)
+ public async Task SeedAsync(CancellationToken cancellationToken)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
- IEnumerable contentCacheNodes = _databaseCacheRepository.GetContentByContentTypeKey(contentTypeKeys);
- foreach (ContentCacheNode contentCacheNode in contentCacheNodes)
+
+ foreach (Guid key in SeedKeys)
{
- if (contentCacheNode.IsDraft)
+ if(cancellationToken.IsCancellationRequested)
{
- continue;
+ break;
}
- // 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),
- };
+ var cacheKey = GetCacheKey(key, false);
- await _hybridCache.SetAsync(
- GetCacheKey(contentCacheNode.Key, false),
- contentCacheNode,
- entryOptions);
+ // We'll use GetOrCreateAsync because it may be in the second level cache, in which case we don't have to re-seed.
+ ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync(
+ cacheKey,
+ async cancel =>
+ {
+ ContentCacheNode? cacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, false);
+
+ // We don't want to seed drafts
+ if (cacheNode is null || cacheNode.IsDraft)
+ {
+ return null;
+ }
+
+ return cacheNode;
+ },
+ GetSeedEntryOptions());
+
+ // If the value is null, it's likely because
+ if (cachedValue is null)
+ {
+ await _hybridCache.RemoveAsync(cacheKey);
+ }
}
scope.Complete();
}
+ private HybridCacheEntryOptions GetSeedEntryOptions() => new()
+ {
+ Expiration = _cacheSettings.SeedCacheDuration,
+ LocalCacheExpiration = _cacheSettings.SeedCacheDuration
+ };
+
public async Task HasContentByIdAsync(int id, bool preview = false)
{
Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document);
@@ -122,34 +166,67 @@ internal sealed class DocumentCacheService : IDocumentCacheService
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
+ bool isSeeded = SeedKeys.Contains(content.Key);
+
// Always set draft node
// We have nodes seperate in the cache, cause 99% of the time, you are only using one
// and thus we won't get too much data when retrieving from the cache.
- var draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true);
- await _hybridCache.RemoveAsync(GetCacheKey(content.Key, true));
+ ContentCacheNode draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true);
+
await _databaseCacheRepository.RefreshContentAsync(draftCacheNode, content.PublishedState);
+ _scopeProvider.Context?.Enlist($"UpdateMemoryCache_Draft_{content.Key}", completed =>
+ {
+ if(completed is false)
+ {
+ return;
+ }
+
+ RefreshHybridCache(draftCacheNode, GetCacheKey(content.Key, true), isSeeded).GetAwaiter().GetResult();
+ }, 1);
if (content.PublishedState == PublishedState.Publishing)
{
var publishedCacheNode = _cacheNodeFactory.ToContentCacheNode(content, false);
- await _hybridCache.RemoveAsync(GetCacheKey(content.Key, false));
+
await _databaseCacheRepository.RefreshContentAsync(publishedCacheNode, content.PublishedState);
+ _scopeProvider.Context?.Enlist($"UpdateMemoryCache_{content.Key}", completed =>
+ {
+ if(completed is false)
+ {
+ return;
+ }
+
+ RefreshHybridCache(publishedCacheNode, GetCacheKey(content.Key, false), isSeeded).GetAwaiter().GetResult();
+ }, 1);
}
scope.Complete();
}
+ private async Task RefreshHybridCache(ContentCacheNode cacheNode, string cacheKey, bool isSeeded)
+ {
+ // If it's seeded we want it to stick around the cache for longer.
+ if (isSeeded)
+ {
+ await _hybridCache.SetAsync(
+ cacheKey,
+ cacheNode,
+ GetSeedEntryOptions());
+ }
+ else
+ {
+ await _hybridCache.SetAsync(cacheKey, cacheNode);
+ }
+ }
+
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
- public async Task DeleteItemAsync(int id)
+ public async Task DeleteItemAsync(IContentBase content)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
- await _databaseCacheRepository.DeleteContentItemAsync(id);
- Attempt 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);
+ await _databaseCacheRepository.DeleteContentItemAsync(content.Id);
+ await _hybridCache.RemoveAsync(GetCacheKey(content.Key, true));
+ await _hybridCache.RemoveAsync(GetCacheKey(content.Key, false));
scope.Complete();
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs
index 794c22b261..280e0e97f0 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs
@@ -9,13 +9,13 @@ public interface IDocumentCacheService
Task GetByIdAsync(int id, bool preview = false);
- Task SeedAsync(IReadOnlyCollection contentTypeKeys);
+ Task SeedAsync(CancellationToken cancellationToken);
Task HasContentByIdAsync(int id, bool preview = false);
Task RefreshContentAsync(IContent content);
- Task DeleteItemAsync(int id);
+ Task DeleteItemAsync(IContentBase content);
void Rebuild(IReadOnlyCollection contentTypeKeys);
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs
index ad5ed2d769..bbdf166189 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs
@@ -13,5 +13,7 @@ public interface IMediaCacheService
Task RefreshMediaAsync(IMedia media);
- Task DeleteItemAsync(int id);
+ Task DeleteItemAsync(IContentBase media);
+
+ Task SeedAsync(CancellationToken cancellationToken);
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs
index 9f62072c0d..70f49f9531 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs
@@ -1,4 +1,6 @@
-using Umbraco.Cms.Core;
+using Microsoft.Extensions.Caching.Hybrid;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Scoping;
@@ -16,6 +18,29 @@ internal class MediaCacheService : IMediaCacheService
private readonly Microsoft.Extensions.Caching.Hybrid.HybridCache _hybridCache;
private readonly IPublishedContentFactory _publishedContentFactory;
private readonly ICacheNodeFactory _cacheNodeFactory;
+ private readonly IEnumerable _seedKeyProviders;
+ private readonly CacheSettings _cacheSettings;
+
+ private HashSet? _seedKeys;
+ private HashSet SeedKeys
+ {
+ get
+ {
+ if (_seedKeys is not null)
+ {
+ return _seedKeys;
+ }
+
+ _seedKeys = [];
+
+ foreach (IMediaSeedKeyProvider provider in _seedKeyProviders)
+ {
+ _seedKeys.UnionWith(provider.GetSeedKeys());
+ }
+
+ return _seedKeys;
+ }
+ }
public MediaCacheService(
IDatabaseCacheRepository databaseCacheRepository,
@@ -23,7 +48,9 @@ internal class MediaCacheService : IMediaCacheService
ICoreScopeProvider scopeProvider,
Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache,
IPublishedContentFactory publishedContentFactory,
- ICacheNodeFactory cacheNodeFactory)
+ ICacheNodeFactory cacheNodeFactory,
+ IEnumerable seedKeyProviders,
+ IOptions cacheSettings)
{
_databaseCacheRepository = databaseCacheRepository;
_idKeyMap = idKeyMap;
@@ -31,6 +58,8 @@ internal class MediaCacheService : IMediaCacheService
_hybridCache = hybridCache;
_publishedContentFactory = publishedContentFactory;
_cacheNodeFactory = cacheNodeFactory;
+ _seedKeyProviders = seedKeyProviders;
+ _cacheSettings = cacheSettings.Value;
}
public async Task GetByKeyAsync(Guid key)
@@ -100,21 +129,45 @@ internal class MediaCacheService : IMediaCacheService
scope.Complete();
}
- public async Task DeleteItemAsync(int id)
+ public async Task DeleteItemAsync(IContentBase media)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
- await _databaseCacheRepository.DeleteContentItemAsync(id);
- Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Media);
- if (keyAttempt.Success)
- {
- await _hybridCache.RemoveAsync(keyAttempt.Result.ToString());
- }
+ await _databaseCacheRepository.DeleteContentItemAsync(media.Id);
+ await _hybridCache.RemoveAsync(media.Key.ToString());
+ scope.Complete();
+ }
- _idKeyMap.ClearCache(keyAttempt.Result);
- _idKeyMap.ClearCache(id);
+ public async Task SeedAsync(CancellationToken cancellationToken)
+ {
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+
+ foreach (Guid key in SeedKeys)
+ {
+ if(cancellationToken.IsCancellationRequested)
+ {
+ break;
+ }
+
+ var cacheKey = GetCacheKey(key, false);
+
+ ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync(
+ cacheKey,
+ async cancel => await _databaseCacheRepository.GetMediaSourceAsync(key),
+ GetSeedEntryOptions());
+
+ if (cachedValue is null)
+ {
+ await _hybridCache.RemoveAsync(cacheKey);
+ }
+ }
scope.Complete();
}
+ private HybridCacheEntryOptions GetSeedEntryOptions() => new()
+ {
+ Expiration = _cacheSettings.SeedCacheDuration, LocalCacheExpiration = _cacheSettings.SeedCacheDuration,
+ };
+
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj b/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj
index 41fb4becbc..6068233712 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj
+++ b/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj
@@ -25,6 +25,9 @@
<_Parameter1>DynamicProxyGenAssembly2
+
+ <_Parameter1>Umbraco.Tests.UnitTests
+
diff --git a/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs
index 92f65bbc39..069a0d82b2 100644
--- a/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs
+++ b/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs
@@ -1,7 +1,6 @@
// 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;
@@ -17,10 +16,10 @@ public class ContentEditingBuilder
IWithKeyBuilder,
IWithContentTypeKeyBuilder,
IWithParentKeyBuilder,
- IWithTemplateKeyBuilder
+ IWithTemplateKeyBuilder,
+ IBuildContentTypes
{
- private IContentType _contentType;
- private ContentTypeBuilder _contentTypeBuilder;
+ private ContentTypeEditingBuilder _contentTypeEditingBuilder;
private IEnumerable _invariantProperties = [];
private IEnumerable _variants = [];
private Guid _contentTypeKey;
@@ -84,8 +83,7 @@ public class ContentEditingBuilder
return this;
}
- public ContentEditingBuilder AddVariant(string culture, string segment, string name,
- IEnumerable properties)
+ public ContentEditingBuilder AddVariant(string culture, string segment, string name, IEnumerable properties)
{
var variant = new VariantModel { Culture = culture, Segment = segment, Name = name, Properties = properties };
_variants = _variants.Concat(new[] { variant });
@@ -104,13 +102,6 @@ public class ContentEditingBuilder
return this;
}
- public ContentEditingBuilder WithContentType(IContentType contentType)
- {
- _contentTypeBuilder = null;
- _contentType = contentType;
- return this;
- }
-
public override ContentCreateModel Build()
{
var key = _key ?? Guid.NewGuid();
@@ -120,15 +111,7 @@ public class ContentEditingBuilder
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)
{
@@ -140,7 +123,7 @@ public class ContentEditingBuilder
content.TemplateKey = templateKey;
}
- content.ContentTypeKey = contentType.Key;
+ content.ContentTypeKey = _contentTypeKey;
content.Key = key;
content.InvariantProperties = invariantProperties;
content.Variants = variants;
@@ -148,25 +131,39 @@ public class ContentEditingBuilder
return content;
}
- public static ContentCreateModel CreateBasicContent(IContentType contentType, Guid? key) =>
+ public static ContentCreateModel CreateBasicContent(Guid contentTypeKey, Guid? key) =>
new ContentEditingBuilder()
.WithKey(key)
- .WithContentType(contentType)
+ .WithContentTypeKey(contentTypeKey)
.WithInvariantName("Home")
.Build();
- public static ContentCreateModel CreateSimpleContent(IContentType contentType) =>
+ public static ContentCreateModel CreateSimpleContent(Guid contentTypeKey) =>
new ContentEditingBuilder()
- .WithContentType(contentType)
+ .WithContentTypeKey(contentTypeKey)
.WithInvariantName("Home")
.WithInvariantProperty("title", "Welcome to our Home page")
.Build();
- public static ContentCreateModel CreateSimpleContent(IContentType contentType, string name, Guid? parentKey) =>
+ public static ContentCreateModel CreateSimpleContent(Guid contentTypeKey, string name, Guid? parentKey) =>
new ContentEditingBuilder()
- .WithContentType(contentType)
+ .WithContentTypeKey(contentTypeKey)
.WithInvariantName(name)
.WithParentKey(parentKey)
.WithInvariantProperty("title", "Welcome to our Home page")
.Build();
+
+ public static ContentCreateModel CreateSimpleContent(Guid contentTypeKey, string name) =>
+ new ContentEditingBuilder()
+ .WithContentTypeKey(contentTypeKey)
+ .WithInvariantName(name)
+ .WithInvariantProperty("title", "Welcome to our Home page")
+ .Build();
+
+ public static ContentCreateModel CreateContentWithTwoVariantProperties(Guid contentTypeKey, string firstCulture, string secondCulture, string propertyAlias, string propertyName) =>
+ new ContentEditingBuilder()
+ .WithContentTypeKey(contentTypeKey)
+ .AddVariant(firstCulture, null, firstCulture, new[] { new PropertyValueModel { Alias = propertyAlias, Value = propertyName } })
+ .AddVariant(secondCulture, null, secondCulture, new[] { new PropertyValueModel { Alias = propertyAlias, Value = propertyName } })
+ .Build();
}
diff --git a/tests/Umbraco.Tests.Common/Builders/ContentTypeEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentTypeEditingBuilder.cs
new file mode 100644
index 0000000000..bcbcbcada0
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/ContentTypeEditingBuilder.cs
@@ -0,0 +1,240 @@
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.ContentTypeEditing;
+using Umbraco.Cms.Tests.Common.Builders.Extensions;
+using Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+namespace Umbraco.Cms.Tests.Common.Builders;
+
+public class ContentTypeEditingBuilder
+ : ContentTypeBaseBuilder,
+ IBuildPropertyTypes
+{
+ private Guid? _key;
+ private Guid? _containerKey;
+ private ContentTypeCleanup _cleanup = new();
+ private IEnumerable _allowedTemplateKeys;
+ private Guid? _defaultTemplateKey;
+ private bool? _allowAtRoot;
+ private bool? _isElement;
+ private bool? _variesByCulture;
+ private bool? _variesBySegment;
+ private readonly List _propertyTypeBuilders = [];
+ private readonly List> _propertyTypeContainerBuilders = [];
+ private readonly List _allowedContentTypeBuilders = [];
+
+ public ContentTypeEditingBuilder()
+ : base(null)
+ {
+ }
+
+ public ContentTypeEditingBuilder(ContentEditingBuilder parentBuilder)
+ : base(parentBuilder)
+ {
+ }
+
+ public ContentTypeEditingBuilder WithDefaultTemplateKey(Guid templateKey)
+ {
+ _defaultTemplateKey = templateKey;
+ return this;
+ }
+
+ public ContentTypeEditingBuilder WithIsElement(bool isElement)
+ {
+ _isElement = isElement;
+ return this;
+ }
+
+ public PropertyTypeContainerBuilder AddPropertyGroup()
+ {
+ var builder = new PropertyTypeContainerBuilder(this);
+ _propertyTypeContainerBuilders.Add(builder);
+ return builder;
+ }
+
+ public PropertyTypeEditingBuilder AddPropertyType()
+ {
+ var builder = new PropertyTypeEditingBuilder(this);
+ _propertyTypeBuilders.Add(builder);
+ return builder;
+ }
+
+
+ public ContentTypeSortBuilder AddAllowedContentType()
+ {
+ var builder = new ContentTypeSortBuilder(this);
+ _allowedContentTypeBuilders.Add(builder);
+ return builder;
+ }
+
+ public ContentTypeEditingBuilder AddAllowedTemplateKeys(IEnumerable templateKeys)
+ {
+ _allowedTemplateKeys = templateKeys;
+ return this;
+ }
+
+ public ContentTypeEditingBuilder WithAllowAtRoot(bool allowAtRoot)
+ {
+ _allowAtRoot = allowAtRoot;
+ return this;
+ }
+
+ public ContentTypeEditingBuilder WithVariesByCulture(bool variesByCulture)
+ {
+ _variesByCulture = variesByCulture;
+ return this;
+ }
+
+ public ContentTypeEditingBuilder WithVariesBySegment(bool variesBySegment)
+ {
+ _variesBySegment = variesBySegment;
+ return this;
+ }
+
+ public override ContentTypeCreateModel Build()
+ {
+ ContentTypeCreateModel contentType = new ContentTypeCreateModel();
+ contentType.Name = GetName();
+ contentType.Alias = GetAlias();
+ contentType.Key = GetKey();
+ contentType.ContainerKey = _containerKey;
+ contentType.Cleanup = _cleanup;
+ contentType.AllowedTemplateKeys = _allowedTemplateKeys ?? Array.Empty();
+ contentType.DefaultTemplateKey = _defaultTemplateKey;
+ contentType.IsElement = _isElement ?? false;
+ contentType.VariesByCulture = _variesByCulture ?? false;
+ contentType.VariesBySegment = _variesBySegment ?? false;
+ contentType.AllowedAsRoot = _allowAtRoot ?? false;
+ contentType.Properties = _propertyTypeBuilders.Select(x => x.Build());
+ contentType.Containers = _propertyTypeContainerBuilders.Select(x => x.Build());
+ contentType.AllowedContentTypes = _allowedContentTypeBuilders.Select(x => x.Build());
+
+ return contentType;
+ }
+
+ public static ContentTypeCreateModel CreateBasicContentType(string alias = "umbTextpage", string name = "TextPage", IContentType parent = null)
+ {
+ var builder = new ContentTypeEditingBuilder();
+ return (ContentTypeCreateModel)builder
+ .WithAlias(alias)
+ .WithName(name)
+ .WithParentContentType(parent)
+ .Build();
+ }
+
+ public static ContentTypeCreateModel CreateSimpleContentType(string alias = "umbTextpage", string name = "TextPage", IContentType parent = null, string propertyGroupName = "Content", Guid? defaultTemplateKey = null)
+ {
+ var containerKey = Guid.NewGuid();
+ var builder = new ContentTypeEditingBuilder();
+ return (ContentTypeCreateModel)builder
+ .WithAlias(alias)
+ .WithName(name)
+ .WithAllowAtRoot(true)
+ .WithParentContentType(parent)
+ .AddPropertyGroup()
+ .WithKey(containerKey)
+ .WithName(propertyGroupName)
+ .Done()
+ .AddPropertyType()
+ .WithAlias("title")
+ .WithDataTypeKey(Constants.DataTypes.Guids.TextareaGuid)
+ .WithName("Title")
+ .WithContainerKey(containerKey)
+ .Done()
+ .WithDefaultTemplateKey(defaultTemplateKey ?? Guid.Empty)
+ .AddAllowedTemplateKeys([defaultTemplateKey ?? Guid.Empty])
+ .Build();
+ }
+
+ public static ContentTypeCreateModel CreateTextPageContentType(string alias = "textPage", string name = "Text Page", Guid defaultTemplateKey = default)
+ {
+ var containerKeyOne = Guid.NewGuid();
+ var containerKeyTwo = Guid.NewGuid();
+
+ var builder = new ContentTypeEditingBuilder();
+ return (ContentTypeCreateModel)builder
+ .WithAlias(alias)
+ .WithName(name)
+ .WithAllowAtRoot(true)
+ .AddPropertyGroup()
+ .WithName("Content")
+ .WithKey(containerKeyOne)
+ .WithSortOrder(1)
+ .Done()
+ .AddPropertyType()
+ .WithAlias("title")
+ .WithName("Title")
+ .WithContainerKey(containerKeyOne)
+ .WithSortOrder(1)
+ .Done()
+ .AddPropertyType()
+ .WithDataTypeKey(Constants.DataTypes.Guids.RichtextEditorGuid)
+ .WithAlias("bodyText")
+ .WithName("Body text")
+ .WithContainerKey(containerKeyOne)
+ .WithSortOrder(2)
+ .Done()
+ .AddPropertyGroup()
+ .WithName("Meta")
+ .WithSortOrder(2)
+ .WithKey(containerKeyTwo)
+ .Done()
+ .AddPropertyType()
+ .WithAlias("keywords")
+ .WithName("Keywords")
+ .WithContainerKey(containerKeyTwo)
+ .WithSortOrder(1)
+ .Done()
+ .AddPropertyType()
+ .WithAlias("description")
+ .WithName("Description")
+ .WithContainerKey(containerKeyTwo)
+ .WithSortOrder(2)
+ .Done()
+ .AddAllowedTemplateKeys([defaultTemplateKey])
+ .WithDefaultTemplateKey(defaultTemplateKey)
+ .Build();
+ }
+
+ public static ContentTypeCreateModel CreateElementType(string alias = "textElement", string name = "Text Element")
+ {
+ var containerKey = Guid.NewGuid();
+ var builder = new ContentTypeEditingBuilder();
+ return (ContentTypeCreateModel)builder
+ .WithAlias(alias)
+ .WithName(name)
+ .WithIsElement(true)
+ .AddPropertyGroup()
+ .WithName("Content")
+ .WithKey(containerKey)
+ .Done()
+ .AddPropertyType()
+ .WithDataTypeKey(Constants.DataTypes.Guids.RichtextEditorGuid)
+ .WithAlias("bodyText")
+ .WithName("Body text")
+ .WithContainerKey(containerKey)
+ .Done()
+ .Build();
+ }
+
+ public static ContentTypeCreateModel CreateContentTypeWithDataTypeKey(Guid dataTypeKey, string alias = "textElement", string name = "Text Element" )
+ {
+ var containerKey = Guid.NewGuid();
+ var builder = new ContentTypeEditingBuilder();
+ return (ContentTypeCreateModel)builder
+ .WithAlias(alias)
+ .WithName(name)
+ .WithIsElement(true)
+ .AddPropertyGroup()
+ .WithName("Content")
+ .WithKey(containerKey)
+ .Done()
+ .AddPropertyType()
+ .WithDataTypeKey(dataTypeKey)
+ .WithAlias("dataType")
+ .WithName("Data Type")
+ .WithContainerKey(containerKey)
+ .Done()
+ .Build();
+ }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs
index 7a4deca5f7..a63205d4d2 100644
--- a/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs
+++ b/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs
@@ -28,6 +28,11 @@ public class ContentTypeSortBuilder
{
}
+ public ContentTypeSortBuilder(ContentTypeEditingBuilder parentBuilder)
+ : base(null)
+ {
+ }
+
string IWithAliasBuilder.Alias
{
get => _alias;
diff --git a/tests/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs b/tests/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs
index 1a4660ed64..f4cc7db311 100644
--- a/tests/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs
+++ b/tests/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs
@@ -80,6 +80,13 @@ public static class BuilderExtensions
return builder;
}
+ public static T WithDataTypeKey(this T builder, Guid key)
+ where T : IWithDataTypeKeyBuilder
+ {
+ builder.DataTypeKey = key;
+ return builder;
+ }
+
public static T WithParentId(this T builder, int parentId)
where T : IWithParentIdBuilder
{
diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWIthContainerKeyBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWIthContainerKeyBuilder.cs
new file mode 100644
index 0000000000..cc184eea48
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWIthContainerKeyBuilder.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+public interface IWIthContainerKeyBuilder
+{
+ Guid? ContainerKey { get; set; }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithDataTypeKeyBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithDataTypeKeyBuilder.cs
new file mode 100644
index 0000000000..ff188b8017
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithDataTypeKeyBuilder.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+public interface IWithDataTypeKeyBuilder
+{
+ Guid? DataTypeKey { get; set; }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithLabelOnTop.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithLabelOnTop.cs
new file mode 100644
index 0000000000..e86c18a1dc
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithLabelOnTop.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+public interface IWithLabelOnTop
+{
+ public bool? LabelOnTop { get; set; }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryBuilder.cs
new file mode 100644
index 0000000000..04bea1ecb9
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryBuilder.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+public interface IWithMandatoryBuilder
+{
+ bool? Mandatory { get; set; }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryMessageBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryMessageBuilder.cs
new file mode 100644
index 0000000000..0f5c510dc0
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryMessageBuilder.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+public interface IWithMandatoryMessageBuilder
+{
+ string MandatoryMessage { get; set; }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionBuilder.cs
new file mode 100644
index 0000000000..b6cdc81651
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionBuilder.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+public interface IWithRegularExpressionBuilder
+{
+ string RegularExpression { get; set; }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionMessage.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionMessage.cs
new file mode 100644
index 0000000000..6d2e0eef66
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionMessage.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+public interface IWithRegularExpressionMessage
+{
+ string RegularExpressionMessage { get; set; }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithTypeBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithTypeBuilder.cs
new file mode 100644
index 0000000000..8d25ce8a55
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithTypeBuilder.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+public interface IWithTypeBuilder
+{
+ public string Type { get; set; }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesByCultureBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesByCultureBuilder.cs
new file mode 100644
index 0000000000..fe347f30c0
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesByCultureBuilder.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+public interface IWithVariesByCultureBuilder
+{
+ bool VariesByCulture { get; set; }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesBySegmentBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesBySegmentBuilder.cs
new file mode 100644
index 0000000000..0d9ea748e1
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesBySegmentBuilder.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+public interface IWithVariesBySegmentBuilder
+{
+ bool VariesBySegment { get; set; }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyTypeAppearanceBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyTypeAppearanceBuilder.cs
new file mode 100644
index 0000000000..4917b2861b
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/PropertyTypeAppearanceBuilder.cs
@@ -0,0 +1,22 @@
+using Umbraco.Cms.Core.Models.ContentTypeEditing;
+using Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+namespace Umbraco.Cms.Tests.Common.Builders;
+
+public class PropertyTypeAppearanceBuilder
+ : ChildBuilderBase, IBuildPropertyTypes, IWithLabelOnTop
+{
+ private bool? _labelOnTop;
+
+ public PropertyTypeAppearanceBuilder(PropertyTypeEditingBuilder parentBuilder) : base(parentBuilder)
+ {
+ }
+
+ bool? IWithLabelOnTop.LabelOnTop
+ {
+ get => _labelOnTop;
+ set => _labelOnTop = value;
+ }
+
+ public override PropertyTypeAppearance Build() => new() { LabelOnTop = _labelOnTop ?? false };
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyTypeContainerBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyTypeContainerBuilder.cs
new file mode 100644
index 0000000000..b5af22eb50
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/PropertyTypeContainerBuilder.cs
@@ -0,0 +1,66 @@
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.ContentTypeEditing;
+using Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+namespace Umbraco.Cms.Tests.Common.Builders;
+
+public class PropertyTypeContainerBuilder(TParent parentBuilder)
+ : ChildBuilderBase(parentBuilder),
+ IBuildPropertyTypes, IWithKeyBuilder, IWithParentKeyBuilder, IWithNameBuilder, IWithTypeBuilder,
+ IWithSortOrderBuilder
+{
+ private Guid? _key;
+ private Guid? _parentKey;
+ private string _name;
+ private string _type;
+ private int? _sortOrder;
+
+ Guid? IWithKeyBuilder.Key
+ {
+ get => _key;
+ set => _key = value;
+ }
+
+ Guid? IWithParentKeyBuilder.ParentKey
+ {
+ get => _parentKey;
+ set => _parentKey = value;
+ }
+
+ string IWithNameBuilder.Name
+ {
+ get => _name;
+ set => _name = value;
+ }
+
+ string IWithTypeBuilder.Type
+ {
+ get => _type;
+ set => _type = value;
+ }
+
+ int? IWithSortOrderBuilder.SortOrder
+ {
+ get => _sortOrder;
+ set => _sortOrder = value;
+ }
+
+ public override ContentTypePropertyContainerModel Build()
+ {
+ var key = _key ?? Guid.NewGuid();
+ var parentKey = _parentKey;
+ var name = _name ?? "Container";
+ var type = _type ?? "Group";
+ var sortOrder = _sortOrder ?? 0;
+
+
+ return new ContentTypePropertyContainerModel
+ {
+ Key = key,
+ ParentKey = parentKey,
+ Name = name,
+ Type = type,
+ SortOrder = sortOrder,
+ };
+ }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyTypeEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyTypeEditingBuilder.cs
new file mode 100644
index 0000000000..301d77f37b
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/PropertyTypeEditingBuilder.cs
@@ -0,0 +1,176 @@
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models.ContentTypeEditing;
+using Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+namespace Umbraco.Cms.Tests.Common.Builders;
+
+public class PropertyTypeEditingBuilder
+ : ChildBuilderBase, IBuildPropertyTypes, IWithKeyBuilder,
+ IWIthContainerKeyBuilder,
+ IWithSortOrderBuilder, IWithAliasBuilder, IWithNameBuilder, IWithDescriptionBuilder, IWithDataTypeKeyBuilder,
+ IWithVariesByCultureBuilder, IWithVariesBySegmentBuilder
+{
+ private Guid? _key;
+ private Guid? _containerKey;
+ private int? _sortOrder;
+ private string _alias;
+ private string? _name;
+ private string? _description;
+ private Guid? _dataTypeKey;
+ private bool _variesByCulture;
+ private bool _variesBySegment;
+ private PropertyTypeValidationEditingBuilder _validationBuilder;
+ private PropertyTypeAppearanceBuilder _appearanceBuilder;
+
+ public PropertyTypeEditingBuilder(ContentTypeEditingBuilder parentBuilder) : base(parentBuilder)
+ {
+ _validationBuilder = new PropertyTypeValidationEditingBuilder(this);
+ _appearanceBuilder = new PropertyTypeAppearanceBuilder(this);
+ }
+
+ Guid? IWithKeyBuilder.Key
+ {
+ get => _key;
+ set => _key = value;
+ }
+
+ Guid? IWIthContainerKeyBuilder.ContainerKey
+ {
+ get => _containerKey;
+ set => _containerKey = value;
+ }
+
+ int? IWithSortOrderBuilder.SortOrder
+ {
+ get => _sortOrder;
+ set => _sortOrder = value;
+ }
+
+ string IWithAliasBuilder.Alias
+ {
+ get => _alias;
+ set => _alias = value;
+ }
+
+ string IWithNameBuilder.Name
+ {
+ get => _name;
+ set => _name = value;
+ }
+
+ string IWithDescriptionBuilder.Description
+ {
+ get => _description;
+ set => _description = value;
+ }
+
+ Guid? IWithDataTypeKeyBuilder.DataTypeKey
+ {
+ get => _dataTypeKey;
+ set => _dataTypeKey = value;
+ }
+
+ bool IWithVariesByCultureBuilder.VariesByCulture
+ {
+ get => _variesByCulture;
+ set => _variesByCulture = value;
+ }
+
+ bool IWithVariesBySegmentBuilder.VariesBySegment
+ {
+ get => _variesBySegment;
+ set => _variesBySegment = value;
+ }
+
+ public PropertyTypeValidationEditingBuilder AddValidation()
+ {
+ var builder = new PropertyTypeValidationEditingBuilder(this);
+ _validationBuilder = builder;
+ return builder;
+ }
+
+ public PropertyTypeAppearanceBuilder AddAppearance()
+ {
+ var builder = new PropertyTypeAppearanceBuilder(this);
+ _appearanceBuilder = builder;
+ return builder;
+ }
+
+ public PropertyTypeEditingBuilder WithContainerKey(Guid? containerKey)
+ {
+ _containerKey = containerKey;
+ return this;
+ }
+
+ public PropertyTypeEditingBuilder WithSortOrder(int sortOrder)
+ {
+ _sortOrder = sortOrder;
+ return this;
+ }
+
+ public PropertyTypeEditingBuilder WithAlias(string alias)
+ {
+ _alias = alias;
+ return this;
+ }
+
+ public PropertyTypeEditingBuilder WithName(string name)
+ {
+ _name = name;
+ return this;
+ }
+
+ public PropertyTypeEditingBuilder WithDescription(string description)
+ {
+ _description = description;
+ return this;
+ }
+
+ public PropertyTypeEditingBuilder WithDataTypeKey(Guid dataTypeKey)
+ {
+ _dataTypeKey = dataTypeKey;
+ return this;
+ }
+
+ public PropertyTypeEditingBuilder WithVariesByCulture(bool variesByCulture)
+ {
+ _variesByCulture = variesByCulture;
+ return this;
+ }
+
+ public PropertyTypeEditingBuilder WithVariesBySegment(bool variesBySegment)
+ {
+ _variesBySegment = variesBySegment;
+ return this;
+ }
+
+ public override ContentTypePropertyTypeModel Build()
+ {
+ var key = _key ?? Guid.NewGuid();
+ var containerKey = _containerKey;
+ var sortOrder = _sortOrder ?? 0;
+ var alias = _alias ?? "title";
+ var name = _name ?? "Title";
+ var description = _description;
+ var dataTypeKey = _dataTypeKey ?? Constants.DataTypes.Guids.TextareaGuid;
+ var variesByCulture = _variesByCulture;
+ var variesBySegment = _variesBySegment;
+ var validation = _validationBuilder.Build();
+ var appearance = _appearanceBuilder.Build();
+
+ return new ContentTypePropertyTypeModel
+ {
+ Key = key,
+ ContainerKey = containerKey,
+ SortOrder = sortOrder,
+ Alias = alias,
+ Name = name,
+ Description = description,
+ DataTypeKey = dataTypeKey,
+ VariesByCulture = variesByCulture,
+ VariesBySegment = variesBySegment,
+ Validation = validation,
+ Appearance = appearance,
+ };
+ }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyTypeValidationEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyTypeValidationEditingBuilder.cs
new file mode 100644
index 0000000000..781463c760
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/PropertyTypeValidationEditingBuilder.cs
@@ -0,0 +1,55 @@
+using Umbraco.Cms.Core.Models.ContentTypeEditing;
+using Umbraco.Cms.Tests.Common.Builders.Interfaces;
+
+namespace Umbraco.Cms.Tests.Common.Builders;
+
+public class PropertyTypeValidationEditingBuilder
+ : ChildBuilderBase, IBuildPropertyTypes, IWithMandatoryBuilder,
+ IWithMandatoryMessageBuilder, IWithRegularExpressionBuilder, IWithRegularExpressionMessage
+{
+ private bool? _mandatory;
+ private string? _mandatoryMessage;
+ private string? _regularExpression;
+ private string? _regularExpressionMessage;
+
+ public PropertyTypeValidationEditingBuilder(PropertyTypeEditingBuilder parentBuilder) : base(parentBuilder)
+ {
+ }
+
+ bool? IWithMandatoryBuilder.Mandatory
+ {
+ get => _mandatory;
+ set => _mandatory = value;
+ }
+
+ string? IWithMandatoryMessageBuilder.MandatoryMessage
+ {
+ get => _mandatoryMessage;
+ set => _mandatoryMessage = value;
+ }
+
+ string? IWithRegularExpressionBuilder.RegularExpression
+ {
+ get => _regularExpression;
+ set => _regularExpression = value;
+ }
+
+ string? IWithRegularExpressionMessage.RegularExpressionMessage
+ {
+ get => _regularExpressionMessage;
+ set => _regularExpressionMessage = value;
+ }
+
+ public override PropertyTypeValidation Build()
+ {
+ var validation = new PropertyTypeValidation
+ {
+ Mandatory = _mandatory ?? false,
+ MandatoryMessage = _mandatoryMessage ?? null,
+ RegularExpression = _regularExpression ?? null,
+ RegularExpressionMessage = _regularExpressionMessage ?? null,
+ };
+
+ return validation;
+ }
+}
diff --git a/tests/Umbraco.Tests.Common/TestHelpers/ContentTypeUpdateHelper.cs b/tests/Umbraco.Tests.Common/TestHelpers/ContentTypeUpdateHelper.cs
new file mode 100644
index 0000000000..d2d083cde7
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/TestHelpers/ContentTypeUpdateHelper.cs
@@ -0,0 +1,83 @@
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.ContentTypeEditing;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Tests.Common.TestHelpers;
+
+public class ContentTypeUpdateHelper
+{
+ public ContentTypeUpdateModel CreateContentTypeUpdateModel(IContentType contentType)
+ {
+ var updateModel = new ContentTypeUpdateModel();
+ var model = MapBaseProperties(contentType, updateModel);
+ return model;
+ }
+
+ private T MapBaseProperties(IContentType contentType, T model) where T : ContentTypeModelBase
+ {
+ model.Alias = contentType.Alias;
+ model.Name = contentType.Name;
+ model.Description = contentType.Description;
+ model.Icon = contentType.Icon;
+ model.AllowedAsRoot = contentType.AllowedAsRoot;
+ model.VariesByCulture = contentType.VariesByCulture();
+ model.VariesBySegment = contentType.VariesBySegment();
+ model.IsElement = contentType.IsElement;
+ model.ListView = contentType.ListView;
+ model.Cleanup = new ContentTypeCleanup()
+ {
+ PreventCleanup = contentType.HistoryCleanup.PreventCleanup,
+ KeepAllVersionsNewerThanDays = contentType.HistoryCleanup.KeepAllVersionsNewerThanDays,
+ KeepLatestVersionPerDayForDays = contentType.HistoryCleanup.KeepLatestVersionPerDayForDays
+ };
+
+ model.AllowedTemplateKeys = contentType.AllowedTemplates.Select(x => x.Key);
+ model.DefaultTemplateKey = contentType.DefaultTemplate?.Key;
+
+ var tempContainerList = model.Containers.ToList();
+
+ foreach (var container in contentType.PropertyGroups)
+ {
+ var containerModel = new ContentTypePropertyContainerModel()
+ {
+ Key = container.Key,
+ Name = container.Name,
+ SortOrder = container.SortOrder,
+ Type = container.Type.ToString()
+ };
+ tempContainerList.Add(containerModel);
+ }
+
+ model.Containers = tempContainerList.AsEnumerable();
+
+ var tempPropertyList = model.Properties.ToList();
+
+ foreach (var propertyType in contentType.PropertyTypes)
+ {
+ var propertyModel = new ContentTypePropertyTypeModel
+ {
+ Key = propertyType.Key,
+ ContainerKey = contentType.PropertyGroups.Single(x => x.PropertyTypes.Contains(propertyType)).Key,
+ SortOrder = propertyType.SortOrder,
+ Alias = propertyType.Alias,
+ Name = propertyType.Name,
+ Description = propertyType.Description,
+ DataTypeKey = propertyType.DataTypeKey,
+ VariesByCulture = propertyType.VariesByCulture(),
+ VariesBySegment = propertyType.VariesBySegment(),
+ Validation = new PropertyTypeValidation()
+ {
+ Mandatory = propertyType.Mandatory,
+ MandatoryMessage = propertyType.ValidationRegExp,
+ RegularExpression = propertyType.ValidationRegExp,
+ RegularExpressionMessage = propertyType.ValidationRegExpMessage,
+ },
+ Appearance = new PropertyTypeAppearance() { LabelOnTop = propertyType.LabelOnTop, }
+ };
+ tempPropertyList.Add(propertyModel);
+ }
+
+ model.Properties = tempPropertyList.AsEnumerable();
+ return model;
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs
index 10dd0cb467..e1f047dd90 100644
--- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs
+++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs
@@ -6,28 +6,31 @@ 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.ContentTypeEditing;
using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Services.ContentTypeEditing;
using Umbraco.Cms.Tests.Common.Builders;
+using Umbraco.Cms.Tests.Common.TestHelpers;
namespace Umbraco.Cms.Tests.Integration.Testing;
public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrationTest
{
- protected IContentTypeService ContentTypeService => GetRequiredService();
+ protected IContentTypeEditingService ContentTypeEditingService => GetRequiredService();
protected ITemplateService TemplateService => GetRequiredService();
- private ContentEditingService ContentEditingService =>
- (ContentEditingService)GetRequiredService();
+ private IContentEditingService ContentEditingService => (IContentEditingService)GetRequiredService();
- private ContentPublishingService ContentPublishingService =>
- (ContentPublishingService)GetRequiredService();
+ private IContentPublishingService ContentPublishingService => (IContentPublishingService)GetRequiredService();
+ protected int TemplateId { get; private set; }
+
+ protected ContentCreateModel Subpage1 { get; private set; }
protected ContentCreateModel Subpage2 { get; private set; }
- protected ContentCreateModel Subpage3 { get; private set; }
- protected ContentCreateModel Subpage { get; private set; }
+ protected ContentCreateModel PublishedTextPage { get; private set; }
protected ContentCreateModel Textpage { get; private set; }
@@ -37,13 +40,17 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat
protected int TextpageId { get; private set; }
- protected int SubpageId { get; private set; }
+ protected int PublishedTextPageId { get; private set; }
+
+ protected int Subpage1Id { get; private set; }
protected int Subpage2Id { get; private set; }
- protected int Subpage3Id { get; private set; }
+ protected ContentTypeCreateModel ContentTypeCreateModel { get; private set; }
- protected ContentType ContentType { get; private set; }
+ protected ContentTypeUpdateModel ContentTypeUpdateModel { get; private set; }
+
+ protected IContentType ContentType { get; private set; }
[SetUp]
public new void Setup() => CreateTestData();
@@ -53,19 +60,24 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat
// 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);
-
+ TemplateId = template.Id;
// 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);
+ ContentTypeCreateModel = ContentTypeEditingBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateKey: template.Key);
+ var contentTypeAttempt = await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel, Constants.Security.SuperUserKey);
+ Assert.IsTrue(contentTypeAttempt.Success);
+
+ var contentTypeResult = contentTypeAttempt.Result;
+ ContentTypeUpdateHelper contentTypeUpdateHelper = new ContentTypeUpdateHelper();
+ ContentTypeUpdateModel = contentTypeUpdateHelper.CreateContentTypeUpdateModel(contentTypeResult); ContentTypeUpdateModel.AllowedContentTypes = new[]
+ {
+ new ContentTypeSort(contentTypeResult.Key, 0, ContentTypeCreateModel.Alias),
+ };
+ var updatedContentTypeResult = await ContentTypeEditingService.UpdateAsync(contentTypeResult, ContentTypeUpdateModel, Constants.Security.SuperUserKey);
+ Assert.IsTrue(updatedContentTypeResult.Success);
+ ContentType = updatedContentTypeResult.Result;
// Create and Save Content "Homepage" based on "umbTextpage" -> 1053
- Textpage = ContentEditingBuilder.CreateSimpleContent(ContentType);
- Textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0");
+ Textpage = ContentEditingBuilder.CreateSimpleContent(ContentType.Key);
var createContentResultTextPage = await ContentEditingService.CreateAsync(Textpage, Constants.Security.SuperUserKey);
Assert.IsTrue(createContentResultTextPage.Success);
@@ -87,24 +99,38 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat
};
// 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);
+ PublishedTextPage = ContentEditingBuilder.CreateSimpleContent(ContentType.Key, "Published Page");
+ var createContentResultPublishPage = await ContentEditingService.CreateAsync(PublishedTextPage, Constants.Security.SuperUserKey);
+ Assert.IsTrue(createContentResultPublishPage.Success);
- if (!Subpage.Key.HasValue)
+ if (!PublishedTextPage.Key.HasValue)
{
throw new InvalidOperationException("The content page key is null.");
}
- if (createContentResultSubPage.Result.Content != null)
+ if (createContentResultPublishPage.Result.Content != null)
{
- SubpageId = createContentResultSubPage.Result.Content.Id;
+ PublishedTextPageId = createContentResultPublishPage.Result.Content.Id;
}
- await ContentPublishingService.PublishAsync(Subpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
+ var publishResult = await ContentPublishingService.PublishAsync(PublishedTextPage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
+ Assert.IsTrue(publishResult.Success);
// Create and Save Content "Text Page 1" based on "umbTextpage" -> 1055
- Subpage2 = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 2", Textpage.Key);
+ Subpage1 = ContentEditingBuilder.CreateSimpleContent(ContentType.Key, "Text Page 1", Textpage.Key);
+ var createContentResultSubPage1 = await ContentEditingService.CreateAsync(Subpage1, Constants.Security.SuperUserKey);
+ Assert.IsTrue(createContentResultSubPage1.Success);
+ if (!Subpage1.Key.HasValue)
+ {
+ throw new InvalidOperationException("The content page key is null.");
+ }
+
+ if (createContentResultSubPage1.Result.Content != null)
+ {
+ Subpage1Id = createContentResultSubPage1.Result.Content.Id;
+ }
+
+ Subpage2 = ContentEditingBuilder.CreateSimpleContent(ContentType.Key, "Text Page 2", Textpage.Key);
var createContentResultSubPage2 = await ContentEditingService.CreateAsync(Subpage2, Constants.Security.SuperUserKey);
Assert.IsTrue(createContentResultSubPage2.Success);
if (!Subpage2.Key.HasValue)
@@ -116,18 +142,5 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat
{
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;
- }
}
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs
new file mode 100644
index 0000000000..7c4b62f2af
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs
@@ -0,0 +1,63 @@
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models.ContentTypeEditing;
+using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.PublishedCache;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Tests.Common.TestHelpers;
+using Umbraco.Cms.Tests.Common.Testing;
+using Umbraco.Cms.Tests.Integration.Testing;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Cache;
+
+[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 PublishedContentTypeCacheTests : UmbracoIntegrationTestWithContentEditing
+{
+ protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
+
+ private IPublishedContentTypeCache PublishedContentTypeCache => GetRequiredService();
+ private IContentTypeService ContentTypeService => GetRequiredService();
+
+ [Test]
+ public async Task Can_Get_Published_DocumentType_By_Key()
+ {
+ // Act
+ var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, ContentType.Key);
+
+ // Assert
+ Assert.IsNotNull(contentType);
+ }
+
+ [Test]
+ public async Task Can_Get_Updated_Published_DocumentType_By_Key()
+ {
+ // Arrange
+ var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey);
+ Assert.IsNotNull(contentType);
+ Assert.AreEqual(1, ContentType.PropertyTypes.Count());
+ // Update the content type
+ ContentTypeUpdateHelper contentTypeUpdateHelper = new ContentTypeUpdateHelper();
+ var updateModel = contentTypeUpdateHelper.CreateContentTypeUpdateModel(ContentType);
+ updateModel.Properties = new List();
+ await ContentTypeEditingService.UpdateAsync(ContentType, updateModel, Constants.Security.SuperUserKey);
+
+ // Act
+ var updatedContentType = PublishedContentTypeCache.Get(PublishedItemType.Content, ContentType.Key);
+
+ // Assert
+ Assert.IsNotNull(updatedContentType);
+ Assert.AreEqual(0, updatedContentType.PropertyTypes.Count());
+ }
+
+ [Test]
+ public async Task Published_DocumentType_Gets_Deleted()
+ {
+ var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, ContentType.Key);
+ Assert.IsNotNull(contentType);
+
+ await ContentTypeService.DeleteAsync(contentType.Key, Constants.Security.SuperUserKey);
+ Assert.Catch(() => PublishedContentTypeCache.Get(PublishedItemType.Content, ContentType.Key));
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs
index f0c70c5911..57503c71f2 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs
@@ -1,7 +1,7 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
-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;
@@ -16,16 +16,16 @@ public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithCo
private IPublishedContentCache PublishedContentHybridCache => GetRequiredService();
- private IPublishedContentTypeCache PublishedContentTypeCache => GetRequiredService();
+ private IContentTypeService ContentTypeService => GetRequiredService();
[Test]
public async Task Can_Get_Draft_Content_By_Id()
{
//Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
+ await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
ContentType.RemovePropertyType("title");
- ContentTypeService.Save(ContentType);
+ await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey);
// Assert
var newTextPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
@@ -36,10 +36,10 @@ public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithCo
public async Task Can_Get_Draft_Content_By_Key()
{
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
+ await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
ContentType.RemovePropertyType("title");
- ContentTypeService.Save(ContentType);
+ await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey);
//Assert
var newTextPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
Assert.IsNull(newTextPage.Value("title"));
@@ -57,26 +57,4 @@ public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithCo
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));
- }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs
index ac7c55604f..5fc467f2f6 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs
@@ -1,4 +1,5 @@
-using Moq;
+using Microsoft.Extensions.Options;
+using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
@@ -7,9 +8,11 @@ using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Infrastructure.HybridCache;
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
using Umbraco.Cms.Infrastructure.HybridCache.Persistence;
+using Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document;
using Umbraco.Cms.Infrastructure.HybridCache.Services;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
@@ -29,6 +32,8 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent
private IContentPublishingService ContentPublishingService => GetRequiredService();
+ private CacheSettings _cacheSettings;
+
[SetUp]
public void SetUp()
{
@@ -44,33 +49,48 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent
false,
new Dictionary(),
null);
- _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(
- new ContentCacheNode()
- {
- ContentTypeId = Textpage.ContentTypeId,
- CreatorId = Textpage.CreatorId,
- CreateDate = Textpage.CreateDate,
- Id = Textpage.Id,
- Key = Textpage.Key,
- SortOrder = 0,
- Data = contentData,
- IsDraft = true,
- });
+
+
+ var draftTestCacheNode = new ContentCacheNode()
+ {
+ ContentTypeId = Textpage.ContentTypeId,
+ CreatorId = Textpage.CreatorId,
+ CreateDate = Textpage.CreateDate,
+ Id = Textpage.Id,
+ Key = Textpage.Key,
+ SortOrder = 0,
+ Data = contentData,
+ IsDraft = true,
+ };
+
+ var publishedTestCacheNode = new ContentCacheNode()
+ {
+ 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.GetContentSourceAsync(It.IsAny(), true))
+ .ReturnsAsync(draftTestCacheNode);
+
+ _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), false))
+ .ReturnsAsync(publishedTestCacheNode);
+
+ _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), true))
+ .ReturnsAsync(draftTestCacheNode);
+
+ _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), false))
+ .ReturnsAsync(publishedTestCacheNode);
_mockedNucacheRepository.Setup(r => r.GetContentByContentTypeKey(It.IsAny>())).Returns(
new List()
{
- new()
- {
- ContentTypeId = Textpage.ContentTypeId,
- CreatorId = Textpage.CreatorId,
- CreateDate = Textpage.CreateDate,
- Id = Textpage.Id,
- Key = Textpage.Key,
- SortOrder = 0,
- Data = contentData,
- IsDraft = false,
- },
+ draftTestCacheNode,
});
_mockedNucacheRepository.Setup(r => r.DeleteContentItemAsync(It.IsAny()));
@@ -81,11 +101,30 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent
GetRequiredService(),
GetRequiredService(),
GetRequiredService(),
- GetRequiredService());
+ GetRequiredService(),
+ GetSeedProviders(),
+ Options.Create(new CacheSettings()));
_mockedCache = new DocumentCache(_mockDocumentCacheService, GetRequiredService());
}
+ // We want to be able to alter the settings for the providers AFTER the test has started
+ // So we'll manually create them with a magic options mock.
+ private IEnumerable GetSeedProviders()
+ {
+ _cacheSettings = new CacheSettings();
+ _cacheSettings.DocumentBreadthFirstSeedCount = 0;
+
+ var mock = new Mock>();
+ mock.Setup(m => m.Value).Returns(() => _cacheSettings);
+
+ return new List
+ {
+ new ContentTypeSeedKeyProvider(GetRequiredService(), GetRequiredService(), mock.Object),
+ new DocumentBreadthFirstKeyProvider(GetRequiredService(), mock.Object),
+ };
+ }
+
[Test]
public async Task Content_Is_Cached_By_Key()
{
@@ -95,7 +134,7 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent
var textPage2 = await _mockedCache.GetByIdAsync(Textpage.Key, true);
AssertTextPage(textPage);
AssertTextPage(textPage2);
- _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1));
+ _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1));
}
[Test]
@@ -121,9 +160,10 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent
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.DeleteItemAsync(Textpage);
- await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key});
+ _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ];
+ await _mockDocumentCacheService.SeedAsync();
var textPage = await _mockedCache.GetByIdAsync(Textpage.Id);
AssertTextPage(textPage);
@@ -141,9 +181,10 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent
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.DeleteItemAsync(Textpage);
- await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key});
+ _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ];
+ await _mockDocumentCacheService.SeedAsync();
var textPage = await _mockedCache.GetByIdAsync(Textpage.Key);
AssertTextPage(textPage);
@@ -151,12 +192,13 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent
}
[Test]
- public async Task Content_Is_Not_Seeded_If_Unpublished_By_Id()
+ public async Task Content_Is_Not_Seeded_If_Unpblished_By_Id()
{
- await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id);
+ await _mockDocumentCacheService.DeleteItemAsync(Textpage);
- await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key});
+ _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ];
+ await _mockDocumentCacheService.SeedAsync();
var textPage = await _mockedCache.GetByIdAsync(Textpage.Id, true);
AssertTextPage(textPage);
@@ -166,13 +208,14 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent
[Test]
public async Task Content_Is_Not_Seeded_If_Unpublished_By_Key()
{
- await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id);
+ _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ];
+ await _mockDocumentCacheService.DeleteItemAsync(Textpage);
- await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key});
+ await _mockDocumentCacheService.SeedAsync();
var textPage = await _mockedCache.GetByIdAsync(Textpage.Key, true);
AssertTextPage(textPage);
- _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1));
+ _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1));
}
private void AssertTextPage(IPublishedContent textPage)
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs
index 0cfc342917..68eddf35df 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs
@@ -6,6 +6,7 @@ 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.ContentTypeEditing;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
@@ -25,23 +26,25 @@ public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest
private ITemplateService TemplateService => GetRequiredService();
- private IContentTypeService ContentTypeService => GetRequiredService();
-
private IContentEditingService ContentEditingService => GetRequiredService();
- private IContentPublishingService ContentPublishingService => GetRequiredService();
+ private IContentTypeEditingService ContentTypeEditingService => GetRequiredService();
+ private IContentPublishingService ContentPublishingService => GetRequiredService();
[Test]
public async Task Can_Get_Value_From_ContentPicker()
{
+ // Arrange
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 textPage = await CreateTextPageDocument(template.Key);
+ var contentPickerDocument = await CreateContentPickerDocument(template.Key, textPage.Key);
+ // Act
var contentPickerPage = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id);
+ // Assert
IPublishedContent contentPickerValue = (IPublishedContent)contentPickerPage.Value("contentPicker");
Assert.AreEqual(textPage.Key, contentPickerValue.Key);
Assert.AreEqual(textPage.Id, contentPickerValue.Id);
@@ -52,10 +55,11 @@ public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest
[Test]
public async Task Can_Get_Value_From_Updated_ContentPicker()
{
+ // Arrange
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 textPage = await CreateTextPageDocument(template.Key);
+ var contentPickerDocument = await CreateContentPickerDocument(template.Key, textPage.Key);
// Get for caching
var notUpdatedContent = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id);
@@ -88,46 +92,42 @@ public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest
Assert.IsTrue(publishResult);
+ // Act
var contentPickerPage = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id);
+
+ // Assert
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 CreateContentPickerDocument(int templateId, Guid textPageKey)
+ private async Task CreateContentPickerDocument(Guid templateKey, Guid textPageKey)
{
- var builder = new ContentTypeBuilder();
- var pickerContentType = (ContentType)builder
+ var builder = new ContentTypeEditingBuilder();
+ var pickerContentType = builder
.WithAlias("test")
.WithName("TestName")
- .AddAllowedTemplate()
- .WithId(templateId)
- .Done()
+ .WithAllowAtRoot(true)
+ .AddAllowedTemplateKeys([templateKey])
.AddPropertyGroup()
- .WithName("Content")
- .WithSupportsPublishing(true)
+ .WithName("Content")
+ .Done()
.AddPropertyType()
- .WithAlias("contentPicker")
- .WithName("Content Picker")
- .WithDataTypeId(1046)
- .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.ContentPicker)
- .WithValueStorageType(ValueStorageType.Integer)
- .WithSortOrder(16)
- .Done()
- .Done()
+ .WithAlias("contentPicker")
+ .WithName("Content Picker")
+ .WithDataTypeKey(Constants.DataTypes.Guids.ContentPickerGuid)
+ .WithSortOrder(16)
+ .Done()
.Build();
- pickerContentType.AllowedAsRoot = true;
- ContentTypeService.Save(pickerContentType);
+ await ContentTypeEditingService.CreateAsync(pickerContentType, Constants.Security.SuperUserKey);
var createOtherModel = new ContentCreateModel
{
- ContentTypeKey = pickerContentType.Key,
+ ContentTypeKey = pickerContentType.Key.Value,
ParentKey = Constants.System.RootKey,
InvariantName = "Test Create",
InvariantProperties = new[] { new PropertyValueModel { Alias = "contentPicker", Value = textPageKey }, },
@@ -149,15 +149,14 @@ public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest
return result.Result.Content;
}
- private async Task CreateTextPageDocument(int templateId)
+ private async Task CreateTextPageDocument(Guid templateKey)
{
- var textContentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: templateId);
- textContentType.AllowedAsRoot = true;
- ContentTypeService.Save(textContentType);
+ var textContentType = ContentTypeEditingBuilder.CreateTextPageContentType(defaultTemplateKey: templateKey);
+ await ContentTypeEditingService.CreateAsync(textContentType, Constants.Security.SuperUserKey);
var createModel = new ContentCreateModel
{
- ContentTypeKey = textContentType.Key,
+ ContentTypeKey = textContentType.Key.Value,
ParentKey = Constants.System.RootKey,
InvariantName = "Root Create",
InvariantProperties = new[]
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs
new file mode 100644
index 0000000000..d7d04b64fb
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs
@@ -0,0 +1,44 @@
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.PublishedCache;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Services.OperationStatus;
+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 DocumentHybridCacheTemplateTests : UmbracoIntegrationTestWithContentEditing
+{
+ protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
+
+ private IPublishedContentCache PublishedContentHybridCache => GetRequiredService();
+
+ private IContentEditingService ContentEditingService => GetRequiredService();
+
+ [Test]
+ public async Task Can_Get_Document_After_Removing_Template()
+ {
+ // Arrange
+ var textPageBefore = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
+ Assert.AreEqual(textPageBefore.TemplateId, TemplateId);
+ var updateModel = new ContentUpdateModel();
+ {
+ updateModel.TemplateKey = null;
+ updateModel.InvariantName = textPageBefore.Name;
+ }
+
+ // Act
+ var updateContentResult = await ContentEditingService.UpdateAsync(textPageBefore.Key, updateModel, Constants.Security.SuperUserKey);
+
+ // Assert
+ Assert.AreEqual(updateContentResult.Status, ContentEditingOperationStatus.Success);
+ var textPageAfter = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
+ // Should this not be null?
+ Assert.AreEqual(textPageAfter.TemplateId, null);
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs
index 7d8d4123e1..bf2bfaddb4 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs
@@ -25,16 +25,13 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
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
+ // Act
var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
- //Assert
+ // Assert
AssertTextPage(textPage);
}
@@ -51,54 +48,42 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
[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);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId);
// Assert
- AssertTextPage(textPage);
+ AssertPublishedTextPage(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);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value);
// Assert
- AssertTextPage(textPage);
+ AssertPublishedTextPage(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);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true);
// Assert
- AssertTextPage(textPage);
+ AssertPublishedTextPage(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);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true);
// Assert
- AssertTextPage(textPage);
+ AssertPublishedTextPage(textPage);
Assert.IsFalse(textPage.IsPublished());
}
@@ -151,19 +136,18 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
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;
+ PublishedTextPage.InvariantName = NewName;
ContentUpdateModel updateModel = new ContentUpdateModel
{
InvariantName = NewName,
- InvariantProperties = Textpage.InvariantProperties,
- Variants = Textpage.Variants,
- TemplateKey = Textpage.TemplateKey,
+ InvariantProperties = PublishedTextPage.InvariantProperties,
+ Variants = PublishedTextPage.Variants,
+ TemplateKey = PublishedTextPage.TemplateKey,
};
- await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
+ await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey);
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview);
// Assert
Assert.AreEqual(result, NewName.Equals(textPage.Name));
@@ -176,22 +160,18 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
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;
-
+ PublishedTextPage.InvariantName = NewName;
ContentUpdateModel updateModel = new ContentUpdateModel
{
InvariantName = NewName,
- InvariantProperties = Textpage.InvariantProperties,
- Variants = Textpage.Variants,
- TemplateKey = Textpage.TemplateKey,
+ InvariantProperties = PublishedTextPage.InvariantProperties,
+ Variants = PublishedTextPage.Variants,
+ TemplateKey = PublishedTextPage.TemplateKey,
};
-
- await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
+ await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey);
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, preview);
// Assert
Assert.AreEqual(result, NewName.Equals(textPage.Name));
@@ -227,11 +207,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
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;
+ var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value;
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true);
// Assert
Assert.AreEqual(titleValue, textPage.Value("title"));
@@ -241,11 +220,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
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;
+ var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value;
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true);
// Assert
Assert.AreEqual(titleValue, textPage.Value("title"));
@@ -255,11 +233,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
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;
+ var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value;
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true);
// Assert
Assert.AreEqual(titleValue, textPage.Value("title"));
@@ -269,11 +246,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
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;
+ var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value;
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true);
// Assert
Assert.AreEqual(titleValue, textPage.Value("title"));
@@ -284,7 +260,6 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
{
// Arrange
Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle;
-
ContentUpdateModel updateModel = new ContentUpdateModel
{
InvariantName = Textpage.InvariantName,
@@ -292,7 +267,6 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
Variants = Textpage.Variants,
TemplateKey = Textpage.TemplateKey,
};
-
await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
// Act
@@ -307,7 +281,6 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
{
// Arrange
Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle;
-
ContentUpdateModel updateModel = new ContentUpdateModel
{
InvariantName = Textpage.InvariantName,
@@ -315,7 +288,6 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
Variants = Textpage.Variants,
TemplateKey = Textpage.TemplateKey,
};
-
await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
// Act
@@ -329,21 +301,19 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
public async Task Can_Get_Updated_Published_Content_Property_By_Id()
{
// Arrange
- Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle;
-
+ PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle;
ContentUpdateModel updateModel = new ContentUpdateModel
{
- InvariantName = Textpage.InvariantName,
- InvariantProperties = Textpage.InvariantProperties,
- Variants = Textpage.Variants,
- TemplateKey = Textpage.TemplateKey,
+ InvariantName = PublishedTextPage.InvariantName,
+ InvariantProperties = PublishedTextPage.InvariantProperties,
+ Variants = PublishedTextPage.Variants,
+ TemplateKey = PublishedTextPage.TemplateKey,
};
-
- await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
- await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
+ await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey);
+ await ContentPublishingService.PublishAsync(PublishedTextPage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true);
// Assert
Assert.AreEqual(NewTitle, textPage.Value("title"));
@@ -353,21 +323,19 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
public async Task Can_Get_Updated_Published_Content_Property_By_Key()
{
// Arrange
- Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle;
-
+ PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle;
ContentUpdateModel updateModel = new ContentUpdateModel
{
- InvariantName = Textpage.InvariantName,
- InvariantProperties = Textpage.InvariantProperties,
- Variants = Textpage.Variants,
- TemplateKey = Textpage.TemplateKey,
+ InvariantName = PublishedTextPage.InvariantName,
+ InvariantProperties = PublishedTextPage.InvariantProperties,
+ Variants = PublishedTextPage.Variants,
+ TemplateKey = PublishedTextPage.TemplateKey,
};
-
- await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
- await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
+ await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey);
+ await ContentPublishingService.PublishAsync(PublishedTextPage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey);
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value);
// Assert
Assert.AreEqual(NewTitle, textPage.Value("title"));
@@ -379,21 +347,18 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
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;
-
+ PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle;
ContentUpdateModel updateModel = new ContentUpdateModel
{
- InvariantName = Textpage.InvariantName,
- InvariantProperties = Textpage.InvariantProperties,
- Variants = Textpage.Variants,
- TemplateKey = Textpage.TemplateKey,
+ InvariantName = PublishedTextPage.InvariantName,
+ InvariantProperties = PublishedTextPage.InvariantProperties,
+ Variants = PublishedTextPage.Variants,
+ TemplateKey = PublishedTextPage.TemplateKey,
};
-
- await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
+ await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey);
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview);
// Assert
Assert.AreEqual(titleName, textPage.Value("title"));
@@ -405,21 +370,18 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
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;
-
+ PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = titleName;
ContentUpdateModel updateModel = new ContentUpdateModel
{
- InvariantName = Textpage.InvariantName,
- InvariantProperties = Textpage.InvariantProperties,
- Variants = Textpage.Variants,
- TemplateKey = Textpage.TemplateKey,
+ InvariantName = PublishedTextPage.InvariantName,
+ InvariantProperties = PublishedTextPage.InvariantProperties,
+ Variants = PublishedTextPage.Variants,
+ TemplateKey = PublishedTextPage.TemplateKey,
};
-
- await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey);
+ await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey);
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true);
// Assert
Assert.AreEqual(titleName, textPage.Value("title"));
@@ -429,12 +391,14 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
public async Task Can_Not_Get_Deleted_Content_By_Id()
{
// Arrange
- var content = await PublishedContentHybridCache.GetByIdAsync(Subpage3Id, true);
+ var content = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true);
Assert.IsNotNull(content);
- await ContentEditingService.DeleteAsync(Subpage3.Key.Value, Constants.Security.SuperUserKey);
+ await ContentEditingService.DeleteAsync(Subpage1.Key.Value, Constants.Security.SuperUserKey);
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage3Id, true);
+ var textPagePublishedContent = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, false);
+
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true);
// Assert
Assert.IsNull(textPage);
@@ -444,11 +408,13 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
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);
+ await PublishedContentHybridCache.GetByIdAsync(Subpage1.Key.Value, true);
+ var hasContent = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true);
+ Assert.IsNotNull(hasContent);
+ await ContentEditingService.DeleteAsync(Subpage1.Key.Value, Constants.Security.SuperUserKey);
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage3.Key.Value, true);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage1.Key.Value, true);
// Assert
Assert.IsNull(textPage);
@@ -460,11 +426,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
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);
+ await ContentEditingService.DeleteAsync(PublishedTextPage.Key.Value, Constants.Security.SuperUserKey);
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview);
// Assert
Assert.IsNull(textPage);
@@ -476,11 +441,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
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);
+ await ContentEditingService.DeleteAsync(PublishedTextPage.Key.Value, Constants.Security.SuperUserKey);
// Act
- var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, preview);
// Assert
Assert.IsNull(textPage);
@@ -499,6 +463,19 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
AssertProperties(Textpage.InvariantProperties, textPage.Properties);
}
+ private void AssertPublishedTextPage(IPublishedContent textPage)
+ {
+ Assert.Multiple(() =>
+ {
+ Assert.IsNotNull(textPage);
+ Assert.AreEqual(PublishedTextPage.Key, textPage.Key);
+ Assert.AreEqual(PublishedTextPage.ContentTypeKey, textPage.ContentType.Key);
+ Assert.AreEqual(PublishedTextPage.InvariantName, textPage.Name);
+ });
+
+ AssertProperties(PublishedTextPage.InvariantProperties, textPage.Properties);
+ }
+
private void AssertProperties(IEnumerable propertyCollection, IEnumerable publishedProperties)
{
foreach (var prop in propertyCollection)
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs
index 8f06be20c3..34e69c0344 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs
@@ -4,6 +4,7 @@ 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.Services.ContentTypeEditing;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
@@ -24,12 +25,12 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest
private string _invariantTitleAlias = "invariantTitle";
private string _invariantTitleName = "Invariant Title";
- private IContentTypeService ContentTypeService => GetRequiredService();
-
private ILanguageService LanguageService => GetRequiredService();
private IContentEditingService ContentEditingService => GetRequiredService();
+ private IContentTypeEditingService ContentTypeEditingService => GetRequiredService();
+
private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService();
private IPublishedContentCache PublishedContentHybridCache => GetRequiredService();
@@ -49,31 +50,35 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest
var updatedInvariantTitle = "Updated Invariant Title";
var updatedVariantTitle = "Updated Variant Title";
-
var updateModel = new ContentUpdateModel
{
- InvariantProperties = new[]
- {
- new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle }
- },
- Variants = new []
+ 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 }
- }
+ Properties =
+ new[]
+ {
+ new PropertyValueModel
+ {
+ Alias = _variantTitleAlias, Value = updatedVariantTitle
+ }
+ },
},
new VariantModel
{
Culture = _danishIsoCode,
Name = "Updated Danish Name",
- Properties = new []
+ Properties = new[]
{
- new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle }
+ new PropertyValueModel
+ {
+ Alias = _variantTitleAlias, Value = updatedVariantTitle
+ },
},
},
},
@@ -100,28 +105,29 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest
var updatedInvariantTitle = "Updated Invariant Title";
var updatedVariantTitle = "Updated Invariant Title";
-
var updateModel = new ContentUpdateModel
{
- InvariantProperties = new[]
- {
- new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle }
- },
- Variants = new []
+ InvariantProperties =
+ new[] { new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle } },
+ Variants = new[]
{
new VariantModel
{
Culture = _englishIsoCode,
Name = "Updated English Name",
- Properties = new []
+ Properties = new[]
{
- new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle }
- }
+ new PropertyValueModel
+ {
+ Alias = _variantTitleAlias, Value = updatedVariantTitle
+ },
+ },
},
},
};
- var result = await ContentEditingService.UpdateAsync(VariantPage.Key, updateModel, Constants.Security.SuperUserKey);
+ var result =
+ await ContentEditingService.UpdateAsync(VariantPage.Key, updateModel, Constants.Security.SuperUserKey);
Assert.IsTrue(result.Success);
// Act
@@ -134,59 +140,42 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest
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()
+ var groupKey = Guid.NewGuid();
+ var contentType = new ContentTypeEditingBuilder()
.WithAlias("cultureVariationTest")
.WithName("Culture Variation Test")
- .WithContentVariation(ContentVariation.Culture)
+ .WithAllowAtRoot(true)
+ .WithVariesByCulture(true)
.AddPropertyType()
- .WithAlias(_variantTitleAlias)
- .WithName(_variantTitleName)
- .WithVariations(ContentVariation.Culture)
- .Done()
+ .WithAlias(_variantTitleAlias)
+ .WithName(_variantTitleName)
+ .WithVariesByCulture(true)
+ .WithContainerKey(groupKey)
+ .Done()
.AddPropertyType()
- .WithAlias(_invariantTitleAlias)
- .WithName(_invariantTitleName)
- .WithVariations(ContentVariation.Nothing)
- .Done()
+ .WithAlias(_invariantTitleAlias)
+ .WithName(_invariantTitleName)
+ .WithContainerKey(groupKey)
+ .Done()
+ .AddPropertyGroup()
+ .WithName("content")
+ .WithKey(groupKey)
+ .Done()
.Build();
- contentType.AllowedAsRoot = true;
- ContentTypeService.Save(contentType);
- var rootContentCreateModel = new ContentCreateModel
+ var contentTypeAttempt = await ContentTypeEditingService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+ if (!contentTypeAttempt.Success)
{
- 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 }
- },
- },
- },
- };
+ throw new Exception("Failed to create content type");
+ }
+ var rootContentCreateModel = ContentEditingBuilder.CreateContentWithTwoVariantProperties(contentTypeAttempt.Result.Key, "en-US", "da-DK", _variantTitleAlias, _variantTitleName);
var result = await ContentEditingService.CreateAsync(rootContentCreateModel, Constants.Security.SuperUserKey);
VariantPage = result.Result.Content;
}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs
new file mode 100644
index 0000000000..fef4486863
--- /dev/null
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs
@@ -0,0 +1,117 @@
+using Microsoft.Extensions.Options;
+using Moq;
+using NUnit.Framework;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Services.Navigation;
+using Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document;
+
+namespace Umbraco.Cms.Tests.UnitTests.Umbraco.PublishedCache.HybridCache;
+
+[TestFixture]
+public class DocumentBreadthFirstKeyProviderTests
+{
+
+ [Test]
+ public void ZeroSeedCountReturnsZeroKeys()
+ {
+ // The structure here doesn't matter greatly, it just matters that there is something.
+ var navigationQueryService = new Mock();
+ var rootKey = Guid.NewGuid();
+ IEnumerable rootKeyList = new List { rootKey };
+ IEnumerable rootChildren = new List { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
+ navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true);
+ navigationQueryService.Setup(x => x.TryGetChildrenKeys(It.IsAny(), out rootChildren)).Returns(true);
+
+
+ var cacheSettings = new CacheSettings { DocumentBreadthFirstSeedCount = 0 };
+ var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(cacheSettings));
+
+ var result = sut.GetSeedKeys();
+
+ Assert.Zero(result.Count);
+ }
+
+ [Test]
+ public void OnlyReturnsKeysUpToSeedCount()
+ {
+ // Structure
+ // Root
+ // - Child1
+ // - Child2
+ // - Child3
+ var navigationQueryService = new Mock();
+ var rootKey = Guid.NewGuid();
+ IEnumerable rootKeyList = new List { rootKey };
+ IEnumerable rootChildren = new List { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
+ navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true);
+ navigationQueryService.Setup(x => x.TryGetChildrenKeys(rootKey, out rootChildren)).Returns(true);
+
+ var expected = 3;
+ var cacheSettings = new CacheSettings { DocumentBreadthFirstSeedCount = expected };
+ var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(cacheSettings));
+
+ var result = sut.GetSeedKeys();
+
+ Assert.That(result.Count, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void IsBreadthFirst()
+ {
+ // Structure
+ // Root
+ // - Child1
+ // - GrandChild
+ // - Child2
+ // - Child3
+
+ var navigationQueryService = new Mock();
+ var rootKey = Guid.NewGuid();
+ var child1Key = Guid.NewGuid();
+ var grandChildKey = Guid.NewGuid();
+ IEnumerable rootKeyList = new List { rootKey };
+ IEnumerable rootChildren = new List { child1Key, Guid.NewGuid(), Guid.NewGuid() };
+ IEnumerable grandChildren = new List { grandChildKey };
+ navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true);
+ navigationQueryService.Setup(x => x.TryGetChildrenKeys(rootKey, out rootChildren)).Returns(true);
+ navigationQueryService.Setup(x => x.TryGetChildrenKeys(child1Key, out grandChildren)).Returns(true);
+
+ // This'll get all children but no grandchildren
+ var cacheSettings = new CacheSettings { DocumentBreadthFirstSeedCount = 4 };
+
+ var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(cacheSettings));
+
+ var result = sut.GetSeedKeys();
+
+ Assert.That(result.Contains(grandChildKey), Is.False);
+ }
+
+ [Test]
+ public void CanGetAll()
+ {
+ var navigationQueryService = new Mock();
+ var rootKey = Guid.NewGuid();
+
+
+ IEnumerable rootKeyList = new List { rootKey };
+ var childrenCount = 300;
+ List rootChildren = new List ();
+ for (int i = 0; i < childrenCount; i++)
+ {
+ rootChildren.Add(Guid.NewGuid());
+ }
+
+ IEnumerable childrenEnumerable = rootChildren;
+ navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true);
+ navigationQueryService.Setup(x => x.TryGetChildrenKeys(rootKey, out childrenEnumerable)).Returns(true);
+ var settings = new CacheSettings { DocumentBreadthFirstSeedCount = int.MaxValue };
+
+
+ var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(settings));
+
+ var result = sut.GetSeedKeys();
+
+ var expected = childrenCount + 1; // Root + children
+ Assert.That(result.Count, Is.EqualTo(expected));
+ }
+}