From 734b3cce2caa935ef3158ac47157189ccbdee024 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 27 Sep 2024 09:12:19 +0200 Subject: [PATCH 01/25] Determine urls at save and publish time (#17033) * Started work on service * temp work * temp commit * Temp commit * Added more routing logic * Fixed tests * Refactor and prepare for isdraft * Work on drafts * Fixed tests * Move to enlistment to ensure caches is only updated on scope complete * Clean up and handle null cultures * Added functionality to the INavigationQueryService to get root keys * Added migration * Fixed issue with navigation * Added migration * Temp commit, move to cache refreshers. * Fixed issues * List urls * fix build * Fixed integration tests * Refactor to create new content finder instead of changing the old * rollback wrong commited line * Clean up, and use docuemnt url service for index * Fixed List endpoin * Do not use Navigation service in methods intended by management api * Fixed examine tests * Make methods virtual * Use domain from published request * Use hybrid cache from new content finder * Eliminate nucache usage * Fixed issue with delivery api and url generation * Fixed linux tests * Added hybrid cache to all integration tests --- .../Factories/DocumentUrlFactory.cs | 48 +- .../Services/SqliteSyntaxProvider.cs | 1 + .../Implement/ContentCacheRefresher.cs | 80 ++- .../Implement/DomainCacheRefresher.cs | 7 +- .../DeliveryApi/ApiPublishedContentCache.cs | 69 +- .../UmbracoBuilder.Collections.cs | 4 +- .../DependencyInjection/UmbracoBuilder.cs | 4 + .../Models/ContentBaseExtensions.cs | 5 +- .../Models/ContentRepositoryExtensions.cs | 9 +- .../Models/PublishedDocumentUrlSegment.cs | 9 + .../Persistence/Constants-DatabaseSchema.cs | 1 + .../Repositories/IDocumentUrlRepository.cs | 10 + .../Routing/ContentFinderByUrlNew.cs | 124 ++++ .../Routing/NewDefaultUrlProvider.cs | 284 ++++++++ src/Umbraco.Core/Routing/PublishedRouter.cs | 10 +- src/Umbraco.Core/Routing/UrlProvider.cs | 4 +- src/Umbraco.Core/Services/ContentService.cs | 32 +- .../Services/DocumentUrlService.cs | 670 ++++++++++++++++++ .../Services/DocumentUrlServiceInitializer.cs | 53 ++ .../Services/IDocumentUrlService.cs | 36 + .../Navigation/INavigationQueryService.cs | 27 + .../IRecycleBinNavigationQueryService.cs | 15 + .../Strings/DefaultUrlSegmentProvider.cs | 12 +- .../Strings/IUrlSegmentProvider.cs | 1 + .../UmbracoBuilder.Repositories.cs | 1 + .../Examine/ContentValueSetBuilder.cs | 41 +- .../Install/DatabaseSchemaCreator.cs | 1 + .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../Upgrade/UmbracoPremigrationPlan.cs | 3 + .../Upgrade/V_15_0_0/AddDocumentUrl.cs | 22 + .../DatabaseAnnotations/IndexTypes.cs | 1 + .../Persistence/Dtos/DocumentUrlDto.cs | 41 ++ .../Implement/DocumentRepository.cs | 5 +- .../Implement/DocumentUrlRepository.cs | 131 ++++ .../SqlSyntax/SqlSyntaxProviderBase.cs | 17 +- .../Services/DomainCacheService.cs | 2 +- src/Umbraco.Web.UI.Client | 2 +- .../Testing/UmbracoIntegrationTest.cs | 4 +- .../UmbracoIntegrationTestWithContent.cs | 14 +- .../Services/ContentServiceTests.cs | 8 +- .../Services/DocumentUrlServiceTest.cs | 190 +++++ ...cumentUrlServiceTest_hidetoplevel_false.cs | 117 +++ .../BackOfficeExamineSearcherTests.cs | 6 +- .../UmbracoExamine/ExamineBaseTest.cs | 14 + .../ExamineExternalIndexTests.cs | 6 +- .../UrlAndDomains/DomainAndUrlsTests.cs | 10 + .../PublishedSnapshotServiceTestBase.cs | 33 +- .../DeliveryApi/PublishedContentCacheTests.cs | 40 +- .../Routing/DomainsAndCulturesTests.cs | 2 +- .../Routing/PublishedRouterTests.cs | 5 +- 50 files changed, 2087 insertions(+), 145 deletions(-) create mode 100644 src/Umbraco.Core/Models/PublishedDocumentUrlSegment.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/IDocumentUrlRepository.cs create mode 100644 src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs create mode 100644 src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs create mode 100644 src/Umbraco.Core/Services/DocumentUrlService.cs create mode 100644 src/Umbraco.Core/Services/DocumentUrlServiceInitializer.cs create mode 100644 src/Umbraco.Core/Services/IDocumentUrlService.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddDocumentUrl.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/DocumentUrlDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentUrlRepository.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs index cee548ed5c..459eed6e69 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs @@ -1,6 +1,8 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Api.Management.ViewModels.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; @@ -12,52 +14,18 @@ namespace Umbraco.Cms.Api.Management.Factories; public class DocumentUrlFactory : IDocumentUrlFactory { - private readonly IPublishedRouter _publishedRouter; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly ILanguageService _languageService; - private readonly ILocalizedTextService _localizedTextService; - private readonly IContentService _contentService; - private readonly IVariationContextAccessor _variationContextAccessor; - private readonly ILoggerFactory _loggerFactory; - private readonly UriUtility _uriUtility; - private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IDocumentUrlService _documentUrlService; public DocumentUrlFactory( - IPublishedRouter publishedRouter, - IUmbracoContextAccessor umbracoContextAccessor, - ILanguageService languageService, - ILocalizedTextService localizedTextService, - IContentService contentService, - IVariationContextAccessor variationContextAccessor, - ILoggerFactory loggerFactory, - UriUtility uriUtility, - IPublishedUrlProvider publishedUrlProvider) + IDocumentUrlService documentUrlService) { - _publishedRouter = publishedRouter; - _umbracoContextAccessor = umbracoContextAccessor; - _languageService = languageService; - _localizedTextService = localizedTextService; - _contentService = contentService; - _variationContextAccessor = variationContextAccessor; - _loggerFactory = loggerFactory; - _uriUtility = uriUtility; - _publishedUrlProvider = publishedUrlProvider; + + _documentUrlService = documentUrlService; } public async Task> CreateUrlsAsync(IContent content) { - IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - - IEnumerable urlInfos = await content.GetContentUrlsAsync( - _publishedRouter, - umbracoContext, - _languageService, - _localizedTextService, - _contentService, - _variationContextAccessor, - _loggerFactory.CreateLogger(), - _uriUtility, - _publishedUrlProvider); + IEnumerable urlInfos = await _documentUrlService.ListUrlsAsync(content.Key); return urlInfos .Where(urlInfo => urlInfo.IsUrl) diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs index 0d140feef3..24d4162da3 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs @@ -79,6 +79,7 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase switch (indexTypes) { case IndexTypes.UniqueNonClustered: + case IndexTypes.UniqueClustered: return "UNIQUE"; default: return string.Empty; diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs index 779b22fe68..f0a9ef93e6 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -6,6 +8,7 @@ using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Cache; @@ -14,9 +17,12 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase { private readonly IDomainService _domainService; + private readonly IDocumentUrlService _documentUrlService; + private readonly IDocumentNavigationQueryService _documentNavigationQueryService; private readonly IIdKeyMap _idKeyMap; private readonly IPublishedSnapshotService _publishedSnapshotService; + [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16")] public ContentCacheRefresher( AppCaches appCaches, IJsonSerializer serializer, @@ -25,11 +31,38 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase(), + StaticServiceProvider.Instance.GetRequiredService() + ) + { + + } + + public ContentCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IIdKeyMap idKeyMap, + IDomainService domainService, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory, + IDocumentUrlService documentUrlService, + IDocumentNavigationQueryService documentNavigationQueryService) : base(appCaches, serializer, eventAggregator, factory) { _publishedSnapshotService = publishedSnapshotService; _idKeyMap = idKeyMap; _domainService = domainService; + _documentUrlService = documentUrlService; + _documentNavigationQueryService = documentNavigationQueryService; } #region Indirect @@ -75,7 +108,7 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase(payload.Key)); - _idKeyMap.ClearCache(payload.Id); + // remove those that are in the branch if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) @@ -89,6 +122,16 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase 0) @@ -129,6 +172,41 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase throw new NotSupportedException(); diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs index 9c5030e553..fda11d6a91 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs @@ -10,16 +10,19 @@ namespace Umbraco.Cms.Core.Cache; public sealed class DomainCacheRefresher : PayloadCacheRefresherBase { private readonly IPublishedSnapshotService _publishedSnapshotService; + private readonly IDomainCacheService _domainCacheService; public DomainCacheRefresher( AppCaches appCaches, IJsonSerializer serializer, IPublishedSnapshotService publishedSnapshotService, IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) + ICacheRefresherNotificationFactory factory, + IDomainCacheService domainCacheService) : base(appCaches, serializer, eventAggregator, factory) { _publishedSnapshotService = publishedSnapshotService; + _domainCacheService = domainCacheService; } #region Json @@ -62,6 +65,8 @@ public sealed class DomainCacheRefresher : PayloadCacheRefresherBase deliveryApiSettings) + public ApiPublishedContentCache( + IRequestPreviewService requestPreviewService, + IRequestCultureService requestCultureService, + IOptionsMonitor deliveryApiSettings, + IDocumentUrlService documentUrlService, + IPublishedContentCache publishedContentCache) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; _requestPreviewService = requestPreviewService; + _requestCultureService = requestCultureService; + _documentUrlService = documentUrlService; + _publishedContentCache = publishedContentCache; _deliveryApiSettings = deliveryApiSettings.CurrentValue; deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings); } + public IPublishedContent? GetByRoute(string route) { - IPublishedContentCache? contentCache = GetContentCache(); - if (contentCache == null) + var isPreviewMode = _requestPreviewService.IsPreview(); + + + // Handle the nasty logic with domain document ids in front of paths. + int? documentStartNodeId = null; + if (route.StartsWith("/") is false) { - return null; + var index = route.IndexOf('/'); + + if (index > -1 && int.TryParse(route.Substring(0, index), out var nodeId)) + { + documentStartNodeId = nodeId; + route = route.Substring(index); + } } - IPublishedContent? content = contentCache.GetByRoute(_requestPreviewService.IsPreview(), route); + Guid? documentKey = _documentUrlService.GetDocumentKeyByRoute( + route, + _requestCultureService.GetRequestedCulture(), + documentStartNodeId, + _requestPreviewService.IsPreview() + ); + IPublishedContent? content = documentKey.HasValue + ? _publishedContentCache.GetById(isPreviewMode, documentKey.Value) + : null; + return ContentOrNullIfDisallowed(content); } public IPublishedContent? GetById(Guid contentId) { - IPublishedContentCache? contentCache = GetContentCache(); - if (contentCache == null) - { - return null; - } - - IPublishedContent? content = contentCache.GetById(_requestPreviewService.IsPreview(), contentId); + IPublishedContent? content = _publishedContentCache.GetById(_requestPreviewService.IsPreview(), contentId); return ContentOrNullIfDisallowed(content); } public IEnumerable GetByIds(IEnumerable contentIds) { - IPublishedContentCache? contentCache = GetContentCache(); - if (contentCache == null) - { - return Enumerable.Empty(); - } - return contentIds - .Select(contentId => contentCache.GetById(_requestPreviewService.IsPreview(), contentId)) + .Select(contentId => _publishedContentCache.GetById(_requestPreviewService.IsPreview(), contentId)) .WhereNotNull() .Where(IsAllowedContentType) .ToArray(); } - private IPublishedContentCache? GetContentCache() => - _publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) - ? publishedSnapshot?.Content - : null; - private IPublishedContent? ContentOrNullIfDisallowed(IPublishedContent? content) => content != null && IsAllowedContentType(content) ? content diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index d27a7e676c..d6f7b480aa 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -36,7 +36,7 @@ public static partial class UmbracoBuilderExtensions // devs can then modify this list on application startup builder.ContentFinders() .Append() - .Append() + .Append() .Append() .Append() /*.Append() // disabled, this is an odd finder */ @@ -47,7 +47,7 @@ public static partial class UmbracoBuilderExtensions builder.HealthCheckNotificationMethods().Add(() => builder.TypeLoader.GetTypes()); builder.UrlProviders() .Append() - .Append(); + .Append(); builder.MediaUrlProviders() .Append(); diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index c39f05cc5e..356150536d 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -412,6 +412,10 @@ namespace Umbraco.Cms.Core.DependencyInjection // add validation services Services.AddUnique(); + + // Routing + Services.AddUnique(); + Services.AddHostedService(); } } } diff --git a/src/Umbraco.Core/Models/ContentBaseExtensions.cs b/src/Umbraco.Core/Models/ContentBaseExtensions.cs index 656db0f82f..09aeee2f7d 100644 --- a/src/Umbraco.Core/Models/ContentBaseExtensions.cs +++ b/src/Umbraco.Core/Models/ContentBaseExtensions.cs @@ -17,8 +17,9 @@ public static class ContentBaseExtensions /// /// /// The culture. + /// Whether to get the published or draft. /// The URL segment. - public static string? GetUrlSegment(this IContentBase content, IShortStringHelper shortStringHelper, IEnumerable urlSegmentProviders, string? culture = null) + public static string? GetUrlSegment(this IContentBase content, IShortStringHelper shortStringHelper, IEnumerable urlSegmentProviders, string? culture = null, bool published = true) { if (content == null) { @@ -30,7 +31,7 @@ public static class ContentBaseExtensions throw new ArgumentNullException(nameof(urlSegmentProviders)); } - var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, culture)).FirstOrDefault(u => u != null); + var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, published, culture)).FirstOrDefault(u => u != null); if (url == null) { if (_defaultUrlSegmentProvider == null) diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index 38d97febd5..5d67a4a974 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -296,7 +296,12 @@ public static class ContentRepositoryExtensions /// A value indicating whether it was possible to publish the names and values for the specified /// culture(s). The method may fail if required names are not set, but it does NOT validate property data /// + /// public static bool PublishCulture(this IContent content, CultureImpact? impact) + { + return PublishCulture(content, impact, DateTime.Now); + } + public static bool PublishCulture(this IContent content, CultureImpact? impact, DateTime publishTime) { if (impact == null) { @@ -323,7 +328,7 @@ public static class ContentRepositoryExtensions return false; } - content.SetPublishInfo(culture, name, DateTime.Now); + content.SetPublishInfo(culture, name, publishTime); } } else if (impact.ImpactsOnlyInvariantCulture) @@ -342,7 +347,7 @@ public static class ContentRepositoryExtensions return false; } - content.SetPublishInfo(impact.Culture, name, DateTime.Now); + content.SetPublishInfo(impact.Culture, name, publishTime); } // set values diff --git a/src/Umbraco.Core/Models/PublishedDocumentUrlSegment.cs b/src/Umbraco.Core/Models/PublishedDocumentUrlSegment.cs new file mode 100644 index 0000000000..81451d3223 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedDocumentUrlSegment.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Core.Models; + +public class PublishedDocumentUrlSegment +{ + public required Guid DocumentKey { get; set; } + public required int LanguageId { get; set; } + public required string UrlSegment { get; set; } + public required bool IsDraft { get; set; } +} diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 24e1e62894..c275fdd108 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -32,6 +32,7 @@ public static partial class Constants public const string Document = TableNamePrefix + "Document"; public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation"; public const string DocumentVersion = TableNamePrefix + "DocumentVersion"; + public const string DocumentUrl = TableNamePrefix + "DocumentUrl"; public const string MediaVersion = TableNamePrefix + "MediaVersion"; public const string ContentSchedule = TableNamePrefix + "ContentSchedule"; diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentUrlRepository.cs new file mode 100644 index 0000000000..5a3786aa12 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentUrlRepository.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDocumentUrlRepository +{ + void Save(IEnumerable publishedDocumentUrlSegments); + IEnumerable GetAll(); + void DeleteByDocumentKey(IEnumerable select); +} diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs b/src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs new file mode 100644 index 0000000000..eeaaeef9b9 --- /dev/null +++ b/src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs @@ -0,0 +1,124 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides an implementation of that handles page nice URLs. +/// +/// +/// Handles /foo/bar where /foo/bar is the nice URL of a document. +/// +public class ContentFinderByUrlNew : IContentFinder +{ + private readonly ILogger _logger; + private readonly IPublishedContentCache _publishedContentCache; + private readonly IDocumentUrlService _documentUrlService; + + /// + /// Initializes a new instance of the class. + /// + public ContentFinderByUrlNew( + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + IDocumentUrlService documentUrlService, + IPublishedContentCache publishedContentCache) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _publishedContentCache = publishedContentCache; + _documentUrlService = documentUrlService; + UmbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + } + + /// + /// Gets the + /// + protected IUmbracoContextAccessor UmbracoContextAccessor { get; } + + /// + /// Tries to find and assign an Umbraco document to a PublishedRequest. + /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + public virtual Task TryFindContent(IPublishedRequestBuilder frequest) + { + if (!UmbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? _)) + { + return Task.FromResult(false); + } + + string route; + if (frequest.Domain != null) + { + route = frequest.Domain.ContentId + + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded); + } + else + { + route = frequest.AbsolutePathDecoded; + } + + IPublishedContent? node = FindContent(frequest, route); + return Task.FromResult(node != null); + } + + /// + /// Tries to find an Umbraco document for a PublishedRequest and a route. + /// + /// The document node, or null. + protected IPublishedContent? FindContent(IPublishedRequestBuilder docreq, string route) + { + if (!UmbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) + { + return null; + } + + if (docreq == null) + { + throw new ArgumentNullException(nameof(docreq)); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Test route {Route}", route); + } + + var documentKey = _documentUrlService.GetDocumentKeyByRoute( + docreq.Domain is null ? route : route.Substring(docreq.Domain.ContentId.ToString().Length), + docreq.Culture, + docreq.Domain?.ContentId, + umbracoContext.InPreviewMode + ); + + IPublishedContent? node = null; + if (documentKey.HasValue) + { + node = _publishedContentCache.GetById(umbracoContext.InPreviewMode, documentKey.Value); + //node = umbracoContext.Content?.GetById(umbracoContext.InPreviewMode, documentKey.Value); + } + + if (node != null) + { + docreq.SetPublishedContent(node); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Got content, id={NodeId}", node.Id); + } + } + else + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("No match."); + } + } + + return node; + } +} diff --git a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs new file mode 100644 index 0000000000..c4fa6cfe1d --- /dev/null +++ b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs @@ -0,0 +1,284 @@ +using System.Globalization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides urls. +/// +public class NewDefaultUrlProvider : IUrlProvider +{ + private readonly ILocalizationService _localizationService; + private readonly IPublishedContentCache _publishedContentCache; + private readonly IDomainCache _domainCache; + private readonly IIdKeyMap _idKeyMap; + private readonly IDocumentUrlService _documentUrlService; + private readonly ILocalizedTextService? _localizedTextService; + private readonly ILogger _logger; + private readonly ISiteDomainMapper _siteDomainMapper; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly UriUtility _uriUtility; + private RequestHandlerSettings _requestSettings; + + public NewDefaultUrlProvider( + IOptionsMonitor requestSettings, + ILogger logger, + ISiteDomainMapper siteDomainMapper, + IUmbracoContextAccessor umbracoContextAccessor, + UriUtility uriUtility, + ILocalizationService localizationService, + IPublishedContentCache publishedContentCache, + IDomainCache domainCache, + IIdKeyMap idKeyMap, + IDocumentUrlService documentUrlService) + { + _requestSettings = requestSettings.CurrentValue; + _logger = logger; + _siteDomainMapper = siteDomainMapper; + _umbracoContextAccessor = umbracoContextAccessor; + _uriUtility = uriUtility; + _localizationService = localizationService; + _publishedContentCache = publishedContentCache; + _domainCache = domainCache; + _idKeyMap = idKeyMap; + _documentUrlService = documentUrlService; + + requestSettings.OnChange(x => _requestSettings = x); + } + + #region GetOtherUrls + + /// + /// Gets the other URLs of a published content. + /// + /// The published content id. + /// The current absolute URL. + /// The other URLs for the published content. + /// + /// + /// Other URLs are those that GetUrl would not return in the current context, but would be valid + /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + /// + public virtual IEnumerable GetOtherUrls(int id, Uri current) + { + var keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document); + + if (keyAttempt.Success is false) + { + yield break; + } + + var key = keyAttempt.Result; + + IPublishedContent? node = _publishedContentCache.GetById(key); + if (node == null) + { + yield break; + } + + + + // look for domains, walking up the tree + IPublishedContent? n = node; + IEnumerable? domainUris = + DomainUtilities.DomainsForNode(_domainCache, _siteDomainMapper, n.Id, current, false); + + // n is null at root + while (domainUris == null && n != null) + { + n = n.Parent; // move to parent node + domainUris = n == null + ? null + : DomainUtilities.DomainsForNode(_domainCache, _siteDomainMapper, n.Id, current); + } + + // no domains = exit + if (domainUris == null) + { + yield break; + } + + foreach (DomainAndUri d in domainUris) + { + var culture = d.Culture; + + // although we are passing in culture here, if any node in this path is invariant, it ignores the culture anyways so this is ok + var route = GetLegacyRouteFormatById(key, culture); + if (route == null) + { + continue; + } + + // need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned) + var pos = route.IndexOf('/'); + var path = pos == 0 ? route : route.Substring(pos); + + var uri = new Uri(CombinePaths(d.Uri.GetLeftPart(UriPartial.Path), path)); + uri = _uriUtility.UriFromUmbraco(uri, _requestSettings); + yield return UrlInfo.Url(uri.ToString(), culture); + } + } + + /// + /// Gets the legacy route format by id + /// + /// + /// + /// + /// + /// When no domain is set the route can be something like /child/grandchild + /// When a domain is set, the route can be something like 1234/grandchild + /// + + private string GetLegacyRouteFormatById(Guid key, string? culture) + { + + return _documentUrlService.GetLegacyRouteFormat(key, culture, _umbracoContextAccessor.GetRequiredUmbracoContext().InPreviewMode); + + + } + + #endregion + + #region GetUrl + + /// + public virtual UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current) + { + if (!current.IsAbsoluteUri) + { + throw new ArgumentException("Current URL must be absolute.", nameof(current)); + } + + + // will not use cache if previewing + var route = GetLegacyRouteFormatById(content.Key, culture); + + return GetUrlFromRoute(route, content.Id, current, mode, culture); + } + + internal UrlInfo? GetUrlFromRoute( + string? route, + int id, + Uri current, + UrlMode mode, + string? culture) + { + if (string.IsNullOrWhiteSpace(route)) + { + if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + { + _logger.LogDebug( + "Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.", + id); + } + return null; + } + + // extract domainUri and path + // route is / or / + var pos = route.IndexOf('/'); + var path = pos == 0 ? route : route[pos..]; + DomainAndUri? domainUri = pos == 0 + ? null + : DomainUtilities.DomainForNode( + _domainCache, + _siteDomainMapper, + int.Parse(route[..pos], CultureInfo.InvariantCulture), + current, + culture); + + var defaultCulture = _localizationService.GetDefaultLanguageIsoCode(); + if (domainUri is not null || string.IsNullOrEmpty(culture) || + culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase)) + { + var url = AssembleUrl(domainUri, path, current, mode).ToString(); + return UrlInfo.Url(url, culture); + } + + return null; + } + + #endregion + + #region Utilities + + private Uri AssembleUrl(DomainAndUri? domainUri, string path, Uri current, UrlMode mode) + { + Uri uri; + + // ignore vdir at that point, UriFromUmbraco will do it + // no domain was found + if (domainUri == null) + { + if (current == null) + { + mode = UrlMode.Relative; // best we can do + } + + switch (mode) + { + case UrlMode.Absolute: + uri = new Uri(current!.GetLeftPart(UriPartial.Authority) + path); + break; + case UrlMode.Relative: + case UrlMode.Auto: + uri = new Uri(path, UriKind.Relative); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode)); + } + } + + // a domain was found + else + { + if (mode == UrlMode.Auto) + { + // this check is a little tricky, we can't just compare domains + if (current != null && domainUri.Uri.GetLeftPart(UriPartial.Authority) == + current.GetLeftPart(UriPartial.Authority)) + { + mode = UrlMode.Relative; + } + else + { + mode = UrlMode.Absolute; + } + } + + switch (mode) + { + case UrlMode.Absolute: + uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); + break; + case UrlMode.Relative: + uri = new Uri(CombinePaths(domainUri.Uri.AbsolutePath, path), UriKind.Relative); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode)); + } + } + + // UriFromUmbraco will handle vdir + // meaning it will add vdir into domain URLs too! + return _uriUtility.UriFromUmbraco(uri, _requestSettings); + } + + private string CombinePaths(string path1, string path2) + { + var path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2; + return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash); + } + + #endregion +} diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index df1d459327..28cd4323eb 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -24,6 +24,7 @@ public class PublishedRouter : IPublishedRouter private readonly IContentLastChanceFinder _contentLastChanceFinder; private readonly IContentTypeService _contentTypeService; private readonly IEventAggregator _eventAggregator; + private readonly IDomainCache _domainCache; private readonly IFileService _fileService; private readonly ILogger _logger; private readonly IProfilingLogger _profilingLogger; @@ -50,7 +51,8 @@ public class PublishedRouter : IPublishedRouter IFileService fileService, IContentTypeService contentTypeService, IUmbracoContextAccessor umbracoContextAccessor, - IEventAggregator eventAggregator) + IEventAggregator eventAggregator, + IDomainCache domainCache) { _webRoutingSettings = webRoutingSettings.CurrentValue ?? throw new ArgumentNullException(nameof(webRoutingSettings)); @@ -68,6 +70,7 @@ public class PublishedRouter : IPublishedRouter _contentTypeService = contentTypeService; _umbracoContextAccessor = umbracoContextAccessor; _eventAggregator = eventAggregator; + _domainCache = domainCache; webRoutingSettings.OnChange(x => _webRoutingSettings = x); } @@ -404,11 +407,10 @@ public class PublishedRouter : IPublishedRouter } var rootNodeId = request.Domain != null ? request.Domain.ContentId : (int?)null; - IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); Domain? domain = - DomainUtilities.FindWildcardDomainInPath(umbracoContext.PublishedSnapshot.Domains?.GetAll(true), nodePath, rootNodeId); + DomainUtilities.FindWildcardDomainInPath(_domainCache.GetAll(true), nodePath, rootNodeId); + - // always has a contentId and a culture if (domain != null) { request.SetCulture(domain.Culture); diff --git a/src/Umbraco.Core/Routing/UrlProvider.cs b/src/Umbraco.Core/Routing/UrlProvider.cs index f6c8691622..067c748da1 100644 --- a/src/Umbraco.Core/Routing/UrlProvider.cs +++ b/src/Umbraco.Core/Routing/UrlProvider.cs @@ -133,10 +133,10 @@ namespace Umbraco.Cms.Core.Routing public string GetUrlFromRoute(int id, string? route, string? culture) { IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - DefaultUrlProvider? provider = _urlProviders.OfType().FirstOrDefault(); + NewDefaultUrlProvider? provider = _urlProviders.OfType().FirstOrDefault(); var url = provider == null ? route // what else? - : provider.GetUrlFromRoute(route, umbracoContext, id, umbracoContext.CleanedUmbracoUrl, Mode, culture)?.Text; + : provider.GetUrlFromRoute(route, id, umbracoContext.CleanedUmbracoUrl, Mode, culture)?.Text; return url ?? "#"; } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 9c2af6f5bc..c667ea0b35 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1060,7 +1060,14 @@ public class ContentService : RepositoryService, IContentService // Updates in-memory navigation structure - we only handle new items, other updates are not a concern UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.Save-with-contentSchedule", - () => _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key)); + () => + { + _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key); + if (content.Trashed) + { + _documentNavigationManagementService.MoveToBin(content.Key); + } + }); if (contentSchedule != null) { @@ -1129,7 +1136,14 @@ public class ContentService : RepositoryService, IContentService // Updates in-memory navigation structure - we only handle new items, other updates are not a concern UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.Save", - () => _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key)); + () => + { + _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key); + if (content.Trashed) + { + _documentNavigationManagementService.MoveToBin(content.Key); + } + }); } scope.Notifications.Publish( @@ -1227,9 +1241,10 @@ public class ContentService : RepositoryService, IContentService // publish the culture(s) // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. + var publishTime = DateTime.Now; foreach (CultureImpact? impact in impacts) { - content.PublishCulture(impact); + content.PublishCulture(impact, publishTime); } // Change state to publishing @@ -1866,7 +1881,7 @@ public class ContentService : RepositoryService, IContentService // publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed IProperty[]? invalidProperties = null; CultureImpact impact = _cultureImpactFactory.ImpactExplicit(culture, IsDefaultCulture(allLangs.Value, culture)); - var tryPublish = d.PublishCulture(impact) && + var tryPublish = d.PublishCulture(impact, date) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact); if (invalidProperties != null && invalidProperties.Length > 0) { @@ -1943,17 +1958,19 @@ public class ContentService : RepositoryService, IContentService { // variant content type - publish specified cultures // invariant content type - publish only the invariant culture + + var publishTime = DateTime.Now; if (content.ContentType.VariesByCulture()) { return culturesToPublish.All(culture => { CultureImpact? impact = _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content); - return content.PublishCulture(impact) && + return content.PublishCulture(impact, publishTime) && _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact); }); } - return content.PublishCulture(_cultureImpactFactory.ImpactInvariant()) + return content.PublishCulture(_cultureImpactFactory.ImpactInvariant(), publishTime) && _propertyValidationService.Value.IsPropertyDataValid(content, out _, _cultureImpactFactory.ImpactInvariant()); } @@ -3179,7 +3196,8 @@ public class ContentService : RepositoryService, IContentService .ToArray(); // publish the culture(s) - if (!impactsToPublish.All(content.PublishCulture)) + var publishTime = DateTime.Now; + if (!impactsToPublish.All(impact => content.PublishCulture(impact, publishTime))) { return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content); } diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs new file mode 100644 index 0000000000..bcc0cbed47 --- /dev/null +++ b/src/Umbraco.Core/Services/DocumentUrlService.cs @@ -0,0 +1,670 @@ +using System.Collections.Concurrent; +using System.Globalization; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services; + +public class DocumentUrlService : IDocumentUrlService +{ + private const string RebuildKey = "UmbracoUrlGeneration"; + + private readonly ILogger _logger; + private readonly IDocumentUrlRepository _documentUrlRepository; + private readonly IDocumentRepository _documentRepository; + private readonly ICoreScopeProvider _coreScopeProvider; + private readonly GlobalSettings _globalSettings; + private readonly UrlSegmentProviderCollection _urlSegmentProviderCollection; + private readonly IContentService _contentService; + private readonly IShortStringHelper _shortStringHelper; + private readonly ILanguageService _languageService; + private readonly IKeyValueService _keyValueService; + private readonly IIdKeyMap _idKeyMap; + private readonly IDocumentNavigationQueryService _documentNavigationQueryService; + private readonly IDomainService _domainService; + + private readonly ConcurrentDictionary _cache = new(); + private bool _isInitialized = false; + + public DocumentUrlService( + ILogger logger, + IDocumentUrlRepository documentUrlRepository, + IDocumentRepository documentRepository, + ICoreScopeProvider coreScopeProvider, + IOptions globalSettings, + UrlSegmentProviderCollection urlSegmentProviderCollection, + IContentService contentService, + IShortStringHelper shortStringHelper, + ILanguageService languageService, + IKeyValueService keyValueService, + IIdKeyMap idKeyMap, + IDocumentNavigationQueryService documentNavigationQueryService, + IDomainService domainService) + { + _logger = logger; + _documentUrlRepository = documentUrlRepository; + _documentRepository = documentRepository; + _coreScopeProvider = coreScopeProvider; + _globalSettings = globalSettings.Value; + _urlSegmentProviderCollection = urlSegmentProviderCollection; + _contentService = contentService; + _shortStringHelper = shortStringHelper; + _languageService = languageService; + _keyValueService = keyValueService; + _idKeyMap = idKeyMap; + _documentNavigationQueryService = documentNavigationQueryService; + _domainService = domainService; + } + + public async Task InitAsync(bool forceEmpty, CancellationToken cancellationToken) + { + if (forceEmpty) + { + // We have this use case when umbraco is installing, we know there is no routes. And we can execute the normal logic because the connection string is missing. + _isInitialized = true; + return; + } + + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + if (await ShouldRebuildUrlsAsync()) + { + _logger.LogInformation("Rebuilding all urls."); + await RebuildAllUrlsAsync(); + } + + IEnumerable publishedDocumentUrlSegments = _documentUrlRepository.GetAll(); + + IEnumerable languages = await _languageService.GetAllAsync(); + var languageIdToIsoCode = languages.ToDictionary(x => x.Id, x => x.IsoCode); + foreach (PublishedDocumentUrlSegment publishedDocumentUrlSegment in publishedDocumentUrlSegments) + { + if (cancellationToken.IsCancellationRequested) + { + break; + } + + if (languageIdToIsoCode.TryGetValue(publishedDocumentUrlSegment.LanguageId, out var isoCode)) + { + UpdateCache(_coreScopeProvider.Context!, publishedDocumentUrlSegment, isoCode); + } + } + _isInitialized = true; + scope.Complete(); + } + + private void UpdateCache(IScopeContext scopeContext, PublishedDocumentUrlSegment publishedDocumentUrlSegment, string isoCode) + { + var cacheKey = CreateCacheKey(publishedDocumentUrlSegment.DocumentKey, isoCode, publishedDocumentUrlSegment.IsDraft); + + scopeContext.Enlist("UpdateCache_" + cacheKey, () => + { + PublishedDocumentUrlSegment? existingValue = null; + _cache.TryGetValue(cacheKey, out existingValue); + + if (existingValue is null) + { + if (_cache.TryAdd(cacheKey, publishedDocumentUrlSegment) is false) + { + _logger.LogError("Could not add the document url cache."); + return false; + } + } + else + { + if (_cache.TryUpdate(cacheKey, publishedDocumentUrlSegment, existingValue) is false) + { + _logger.LogError("Could not update the document url cache."); + return false; + } + } + + return true; + }); + + + } + + private void RemoveFromCache(IScopeContext scopeContext, Guid documentKey, string isoCode) + { + var cacheKeyDraft = CreateCacheKey(documentKey, isoCode, true); + + scopeContext.Enlist("RemoveFromCache_" + cacheKeyDraft, () => + { + if (_cache.TryRemove(cacheKeyDraft, out _) is false) + { + _logger.LogDebug("Could not remove the document url cache. But the important thing is that it is not there."); + return false; + } + + return true; + }); + + var cacheKeyPublished = CreateCacheKey(documentKey, isoCode, false); + + scopeContext.Enlist("RemoveFromCache_" + cacheKeyPublished, () => + { + if (_cache.TryRemove(cacheKeyPublished, out _) is false) + { + _logger.LogDebug("Could not remove the document url cache. But the important thing is that it is not there."); + return false; + } + + return true; + }); + + } + + public async Task RebuildAllUrlsAsync() + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + scope.ReadLock(Constants.Locks.ContentTree); + + IEnumerable documents = _documentRepository.GetMany(Array.Empty()); + + await CreateOrUpdateUrlSegmentsAsync(documents); + + _keyValueService.SetValue(RebuildKey, GetCurrentRebuildValue()); + + scope.Complete(); + } + + public Task ShouldRebuildUrlsAsync() + { + var persistedValue = GetPersistedRebuildValue(); + var currentValue = GetCurrentRebuildValue(); + + return Task.FromResult(string.Equals(persistedValue, currentValue) is false); + } + + private string GetCurrentRebuildValue() + { + return string.Join("|", _urlSegmentProviderCollection.Select(x => x.GetType().Name)); + } + + private string? GetPersistedRebuildValue() => _keyValueService.GetValue(RebuildKey); + + public string? GetUrlSegment(Guid documentKey, string culture, bool isDraft) + { + ThrowIfNotInitialized(); + var cacheKey = CreateCacheKey(documentKey, culture, isDraft); + + _cache.TryGetValue(cacheKey, out PublishedDocumentUrlSegment? urlSegment); + + return urlSegment?.UrlSegment; + } + + private void ThrowIfNotInitialized() + { + if (_isInitialized is false) + { + throw new InvalidOperationException("The service needs to be initialized before it can be used."); + } + } + + public async Task CreateOrUpdateUrlSegmentsAsync(IEnumerable documents) + { + if(documents.Any() is false) + { + return; + } + + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + + var toSave = new List(); + var toDelete = new List(); + var allCultures = documents.SelectMany(x => x.AvailableCultures ).Distinct(); + + var languages = await _languageService.GetMultipleAsync(allCultures); + var languageDictionary = languages.ToDictionary(x=>x.IsoCode); + + foreach (IContent document in documents) + { + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("Rebuilding urls for document with key {DocumentKey}", document.Key); + } + + if (document.AvailableCultures.Any()) + { + foreach (var culture in document.AvailableCultures) + { + var language = languageDictionary[culture]; + + HandleCaching(_coreScopeProvider.Context!, document, culture, language, toDelete, toSave); + } + } + else + { + var language = await _languageService.GetDefaultLanguageAsync(); + + HandleCaching(_coreScopeProvider.Context!, document, null, language!, toDelete, toSave); + } + } + + if(toSave.Any()) + { + _documentUrlRepository.Save(toSave); + } + + if(toDelete.Any()) + { + _documentUrlRepository.DeleteByDocumentKey(toDelete); + } + + scope.Complete(); + } + + private void HandleCaching(IScopeContext scopeContext, IContent document, string? culture, ILanguage language, List toDelete, List toSave) + { + var models = GenerateModels(document, culture, language); + + foreach (PublishedDocumentUrlSegment model in models) + { + if (document.Published is false && model.IsDraft is false) + { + continue; + } + + if (document.Trashed) + { + toDelete.Add(model.DocumentKey); + RemoveFromCache(scopeContext, model.DocumentKey, language.IsoCode); + } + else + { + toSave.Add(model); + UpdateCache(scopeContext, model, language.IsoCode); + } + } + } + + private IEnumerable GenerateModels(IContent document, string? culture, ILanguage language) + { + var publishedUrlSegment = document.GetUrlSegment(_shortStringHelper, _urlSegmentProviderCollection, culture, true); + if(publishedUrlSegment.IsNullOrWhiteSpace()) + { + _logger.LogWarning("No published url segment found for document {DocumentKey} in culture {Culture}", document.Key, culture ?? "{null}"); + } + else + { + yield return new PublishedDocumentUrlSegment() + { + DocumentKey = document.Key, LanguageId = language.Id, UrlSegment = publishedUrlSegment, IsDraft = false + }; + } + + var draftUrlSegment = document.GetUrlSegment(_shortStringHelper, _urlSegmentProviderCollection, culture, false); + + if(draftUrlSegment.IsNullOrWhiteSpace()) + { + _logger.LogWarning("No draft url segment found for document {DocumentKey} in culture {Culture}", document.Key, culture ?? "{null}"); + } + else + { + yield return new PublishedDocumentUrlSegment() + { + DocumentKey = document.Key, LanguageId = language.Id, UrlSegment = draftUrlSegment, IsDraft = true + }; + } + } + + public async Task DeleteUrlsFromCacheAsync(IEnumerable documentKeys) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + + IEnumerable languages = await _languageService.GetAllAsync(); + + foreach (ILanguage language in languages) + { + foreach (Guid documentKey in documentKeys) + { + RemoveFromCache(_coreScopeProvider.Context!, documentKey, language.IsoCode); + } + } + + scope.Complete(); + } + + public Guid? GetDocumentKeyByRoute(string route, string? culture, int? documentStartNodeId, bool isDraft) + { + var urlSegments = route.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries); + + // We need to translate legacy int ids to guid keys. + Guid? runnerKey = GetStartNodeKey(documentStartNodeId); + var hideTopLevelNodeFromPath = _globalSettings.HideTopLevelNodeFromPath; + + culture ??= _languageService.GetDefaultIsoCodeAsync().GetAwaiter().GetResult(); + + if (!_globalSettings.ForceCombineUrlPathLeftToRight + && CultureInfo.GetCultureInfo(culture).TextInfo.IsRightToLeft) + { + urlSegments = urlSegments.Reverse().ToArray(); + } + + // If a domain is assigned to this route, we need to follow the url segments + if (runnerKey.HasValue) + { + // If there is no url segments it means the domain root has been requested + if (urlSegments.Length == 0) + { + return runnerKey.Value; + } + + // Otherwise we have to find the child with that segment anc follow that + foreach (var urlSegment in urlSegments) + { + //Get the children of the runnerKey and find the child (if any) with the correct url segment + var childKeys = GetChildKeys(runnerKey.Value); + + runnerKey = GetChildWithUrlSegment(childKeys, urlSegment, culture, isDraft); + + if (runnerKey is null) + { + break; + } + } + + return runnerKey; + } + // If there is no parts, it means it is a root (and no assigned domain) + if(urlSegments.Length == 0) + { + // // if we do not hide the top level and no domain was found, it maens there is no content. + // // TODO we can remove this to keep consistency with the old routing, but it seems incorrect to allow that. + // if (hideTopLevelNodeFromPath is false) + // { + // return null; + // } + + return GetTopMostRootKey(); + } + + // Otherwise we have to find the root items (or child of the first root when hideTopLevelNodeFromPath is true) and follow the url segments in them to get to correct document key + for (var index = 0; index < urlSegments.Length; index++) + { + var urlSegment = urlSegments[index]; + IEnumerable runnerKeys; + if (index == 0) + { + runnerKeys = GetKeysInRoot(hideTopLevelNodeFromPath); + } + else + { + if (runnerKey is null) + { + break; + } + + runnerKeys = GetChildKeys(runnerKey.Value); + } + + runnerKey = GetChildWithUrlSegment(runnerKeys, urlSegment, culture, isDraft); + } + + return runnerKey; + } + + public string GetLegacyRouteFormat(Guid docuemntKey, string? culture, bool isDraft) + { + var documentIdAttempt = _idKeyMap.GetIdForKey(docuemntKey, UmbracoObjectTypes.Document); + + if(documentIdAttempt.Success is false) + { + return "#"; + } + + if (_documentNavigationQueryService.TryGetAncestorsOrSelfKeys(docuemntKey, + out IEnumerable ancestorsOrSelfKeys) is false) + { + return "#"; + } + + var cultureOrDefault = culture ?? _languageService.GetDefaultIsoCodeAsync().GetAwaiter().GetResult(); + + Guid[] ancestorsOrSelfKeysArray = ancestorsOrSelfKeys as Guid[] ?? ancestorsOrSelfKeys.ToArray(); + IDictionary ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray.ToDictionary(x => x, ancestorKey => + { + IEnumerable domains = _domainService.GetAssignedDomainsAsync(ancestorKey, false).GetAwaiter().GetResult(); + return domains.FirstOrDefault(x=>x.LanguageIsoCode == cultureOrDefault); + }); + + var urlSegments = new List(); + + IDomain? foundDomain = null; + + foreach (Guid ancestorOrSelfKey in ancestorsOrSelfKeysArray) + { + if (ancestorOrSelfKeyToDomains.TryGetValue(ancestorOrSelfKey, out IDomain? domain)) + { + if (domain is not null) + { + foundDomain = domain; + break; + } + } + + if (_cache.TryGetValue(CreateCacheKey(ancestorOrSelfKey, cultureOrDefault, isDraft), out PublishedDocumentUrlSegment? publishedDocumentUrlSegment)) + { + urlSegments.Add(publishedDocumentUrlSegment.UrlSegment); + } + + if (foundDomain is not null) + { + break; + } + } + + if (foundDomain is not null) + { + //we found a domain, and not to construct the route in the funny legacy way + return foundDomain.RootContentId + "/" + string.Join("/", urlSegments); + } + + var isRootFirstItem = GetTopMostRootKey() == ancestorsOrSelfKeysArray.Last(); + return GetFullUrl(isRootFirstItem, urlSegments, null); + } + + + public async Task> ListUrlsAsync(Guid contentKey) + { + var result = new List(); + + var documentIdAttempt = _idKeyMap.GetIdForKey(contentKey, UmbracoObjectTypes.Document); + + if(documentIdAttempt.Success is false) + { + return result; + } + + IEnumerable ancestorsOrSelfKeys = contentKey.Yield() + .Concat(_contentService.GetAncestors(documentIdAttempt.Result).Select(x => x.Key).Reverse()); + + IEnumerable languages = await _languageService.GetAllAsync(); + IEnumerable cultures = languages.Select(x=>x.IsoCode); + + + Guid[] ancestorsOrSelfKeysArray = ancestorsOrSelfKeys as Guid[] ?? ancestorsOrSelfKeys.ToArray(); + Dictionary>> ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray.ToDictionary(x => x, async ancestorKey => + { + IEnumerable domains = await _domainService.GetAssignedDomainsAsync(ancestorKey, false); + return domains.ToDictionary(x => x.LanguageIsoCode!); + }); + + var urlSegments = new List(); + foreach (var culture in cultures) + { + IDomain? foundDomain = null; + + foreach (Guid ancestorOrSelfKey in ancestorsOrSelfKeysArray) + { + if (ancestorOrSelfKeyToDomains.TryGetValue(ancestorOrSelfKey, out Task>? domainDictionaryTask)) + { + var domainDictionary = await domainDictionaryTask; + if (domainDictionary.TryGetValue(culture, out IDomain? domain)) + { + foundDomain = domain; + break; + } + } + + if (_cache.TryGetValue(CreateCacheKey(ancestorOrSelfKey, culture, false), out PublishedDocumentUrlSegment? publishedDocumentUrlSegment)) + { + urlSegments.Add(publishedDocumentUrlSegment.UrlSegment); + } + } + + var isRootFirstItem = GetTopMostRootKey() == ancestorsOrSelfKeysArray.Last(); + result.Add(new UrlInfo( + text: GetFullUrl(isRootFirstItem, urlSegments, foundDomain), + isUrl: true, + culture: culture + )); + + } + + return result; + } + + private string GetFullUrl(bool isRootFirstItem, List reversedUrlSegments, IDomain? foundDomain) + { + var urlSegments = new List(reversedUrlSegments); + urlSegments.Reverse(); + + if (foundDomain is not null) + { + return foundDomain.DomainName + string.Join('/', urlSegments); + } + + return '/' + string.Join('/', urlSegments.Skip(_globalSettings.HideTopLevelNodeFromPath && isRootFirstItem ? 1 : 0)); + } + + public async Task CreateOrUpdateUrlSegmentsWithDescendantsAsync(Guid key) + { + var id = _idKeyMap.GetIdForKey(key, UmbracoObjectTypes.Document).Result; + IContent item = _contentService.GetById(id)!; + IEnumerable descendants = _contentService.GetPagedDescendants(id, 0, int.MaxValue, out _); + + await CreateOrUpdateUrlSegmentsAsync(new List(descendants) + { + item + }); + } + + public async Task CreateOrUpdateUrlSegmentsAsync(Guid key) + { + IContent? content = _contentService.GetById(key); + + if (content is not null) + { + await CreateOrUpdateUrlSegmentsAsync(content.Yield()); + } + } + + + //TODO test cases: + // - Find the root, when a domain is set + // - Find a nested child, when a domain is set + + // - Find the root when no domain is set and hideTopLevelNodeFromPath is true + // - Find a nested child of item in the root top when no domain is set and hideTopLevelNodeFromPath is true + // - Find a nested child of item in the root bottom when no domain is set and hideTopLevelNodeFromPath is true + // - Find the root when no domain is set and hideTopLevelNodeFromPath is false + // - Find a nested child of item in the root top when no domain is set and hideTopLevelNodeFromPath is false + // - Find a nested child of item in the root bottom when no domain is set and hideTopLevelNodeFromPath is false + + // - All of the above when having Constants.Conventions.Content.UrlName set to a value + + private IEnumerable GetKeysInRoot(bool addFirstLevelChildren) + { + //TODO replace with something more performand - Should be possible with navigationservice.. + IEnumerable rootKeys = _contentService.GetRootContent().Select(x=>x.Key).ToArray(); + + foreach (Guid rootKey in rootKeys) + { + yield return rootKey; + } + + if (addFirstLevelChildren) + { + foreach (Guid rootKey in rootKeys) + { + IEnumerable childKeys = GetChildKeys(rootKey); + + foreach (Guid childKey in childKeys) + { + yield return childKey; + } + } + } + + } + + private Guid? GetChildWithUrlSegment(IEnumerable childKeys, string urlSegment, string culture, bool isDraft) + { + foreach (Guid childKey in childKeys) + { + var childUrlSegment = GetUrlSegment(childKey, culture, isDraft); + + if (string.Equals(childUrlSegment, urlSegment)) + { + return childKey; + } + } + + return null; + } + + /// + /// Gets the children based on the latest published version of the content. (No aware of things in this scope). + /// + /// The key of the document to get children from. + /// The keys of all the children of the document. + private IEnumerable GetChildKeys(Guid documentKey) + { + if(_documentNavigationQueryService.TryGetChildrenKeys(documentKey, out IEnumerable childrenKeys)) + { + return childrenKeys; + } + + return Enumerable.Empty(); + } + + /// + /// Gets the top most root key. + /// + /// The top most root key. + private Guid? GetTopMostRootKey() + { + if (_documentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys)) + { + return rootKeys.FirstOrDefault(); + } + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string CreateCacheKey(Guid documentKey, string culture, bool isDraft) => $"{documentKey}|{culture}|{isDraft}"; + + private Guid? GetStartNodeKey(int? documentStartNodeId) + { + if (documentStartNodeId is null) + { + return null; + } + + Attempt attempt = _idKeyMap.GetKeyForId(documentStartNodeId.Value, UmbracoObjectTypes.Document); + return attempt.Success ? attempt.Result : null; + } + +} diff --git a/src/Umbraco.Core/Services/DocumentUrlServiceInitializer.cs b/src/Umbraco.Core/Services/DocumentUrlServiceInitializer.cs new file mode 100644 index 0000000000..4c99b9a8ae --- /dev/null +++ b/src/Umbraco.Core/Services/DocumentUrlServiceInitializer.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Hosting; + +namespace Umbraco.Cms.Core.Services; + +public class DocumentUrlServiceInitializer : IHostedLifecycleService +{ + private readonly IDocumentUrlService _documentUrlService; + private readonly IRuntimeState _runtimeState; + + public DocumentUrlServiceInitializer(IDocumentUrlService documentUrlService, IRuntimeState runtimeState) + { + _documentUrlService = documentUrlService; + _runtimeState = runtimeState; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + if (_runtimeState.Level == RuntimeLevel.Upgrade) + { + //Special case on the first upgrade, as the database is not ready yet. + return; + } + + await _documentUrlService.InitAsync( + _runtimeState.Level <= RuntimeLevel.Install, + cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StartingAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StartedAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StoppingAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StoppedAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/src/Umbraco.Core/Services/IDocumentUrlService.cs b/src/Umbraco.Core/Services/IDocumentUrlService.cs new file mode 100644 index 0000000000..91427fa5f0 --- /dev/null +++ b/src/Umbraco.Core/Services/IDocumentUrlService.cs @@ -0,0 +1,36 @@ +using Umbraco.Cms.Core.Media.EmbedProviders; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Routing; + +namespace Umbraco.Cms.Core.Services; + +public interface IDocumentUrlService +{ + + /// + /// Initializes the service and ensure the content in the database is correct with the current configuration. + /// + /// + /// + Task InitAsync(bool forceEmpty, CancellationToken cancellationToken); + + Task RebuildAllUrlsAsync(); + /// + /// Gets the Url from a document key, culture and segment. Preview urls are returned if isPreview is true. + /// + /// The key of the document. + /// The culture code. + /// Whether to get the url of the draft or published document. + /// The url of the document. + string? GetUrlSegment(Guid documentKey, string culture, bool isDraft); + + Task CreateOrUpdateUrlSegmentsAsync(IEnumerable documents); + + Task DeleteUrlsFromCacheAsync(IEnumerable documentKeys); + + Guid? GetDocumentKeyByRoute(string route, string? culture, int? documentStartNodeId, bool isDraft); + Task> ListUrlsAsync(Guid contentKey); + Task CreateOrUpdateUrlSegmentsWithDescendantsAsync(Guid key); + Task CreateOrUpdateUrlSegmentsAsync(Guid key); + string GetLegacyRouteFormat(Guid key, string? culture, bool isDraft); +} diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index 204ec657eb..ad4e7ae150 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -1,3 +1,5 @@ +using Umbraco.Extensions; + namespace Umbraco.Cms.Core.Services.Navigation; /// @@ -14,7 +16,32 @@ public interface INavigationQueryService bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys); + bool TryGetDescendantsKeysOrSelfKeys(Guid childKey, out IEnumerable descendantsOrSelfKeys) + { + if(TryGetDescendantsKeys(childKey, out var descendantsKeys)) + { + descendantsOrSelfKeys = childKey.Yield().Concat(descendantsKeys); + return true; + } + + descendantsOrSelfKeys = Array.Empty(); + return false; + } + + bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys); + bool TryGetAncestorsOrSelfKeys(Guid childKey, out IEnumerable ancestorsOrSelfKeys) + { + if(TryGetAncestorsKeys(childKey, out var ancestorsKeys)) + { + ancestorsOrSelfKeys = childKey.Yield().Concat(ancestorsKeys); + return true; + } + + ancestorsOrSelfKeys = Array.Empty(); + return false; + } + bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys); } diff --git a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs index 0a57f5346c..9741e86e1f 100644 --- a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs @@ -1,3 +1,5 @@ +using Umbraco.Extensions; + namespace Umbraco.Cms.Core.Services.Navigation; /// @@ -12,6 +14,19 @@ public interface IRecycleBinNavigationQueryService bool TryGetDescendantsKeysInBin(Guid parentKey, out IEnumerable descendantsKeys); + bool TryGetDescendantsKeysOrSelfKeysInBin(Guid childKey, out IEnumerable descendantsOrSelfKeys) + { + if(TryGetDescendantsKeysInBin(childKey, out var descendantsKeys)) + { + descendantsOrSelfKeys = childKey.Yield().Concat(descendantsKeys); + return true; + } + + descendantsOrSelfKeys = Array.Empty(); + return false; + } + + bool TryGetAncestorsKeysInBin(Guid childKey, out IEnumerable ancestorsKeys); bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable siblingsKeys); diff --git a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs index 36c0d6e85e..5e0ae176b7 100644 --- a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs @@ -12,21 +12,25 @@ public class DefaultUrlSegmentProvider : IUrlSegmentProvider public DefaultUrlSegmentProvider(IShortStringHelper shortStringHelper) => _shortStringHelper = shortStringHelper; + + public virtual string? GetUrlSegment(IContentBase content, bool published, string? culture = null) => + GetUrlSegmentSource(content, culture, published)?.ToUrlSegment(_shortStringHelper, culture); + /// /// Gets the URL segment for a specified content and culture. /// /// The content. /// The culture. /// The URL segment. - public string? GetUrlSegment(IContentBase content, string? culture = null) => - GetUrlSegmentSource(content, culture)?.ToUrlSegment(_shortStringHelper, culture); + public virtual string? GetUrlSegment(IContentBase content, string? culture = null) => + GetUrlSegmentSource(content, culture, true)?.ToUrlSegment(_shortStringHelper, culture); - private static string? GetUrlSegmentSource(IContentBase content, string? culture) + private static string? GetUrlSegmentSource(IContentBase content, string? culture, bool published) { string? source = null; if (content.HasProperty(Constants.Conventions.Content.UrlName)) { - source = (content.GetValue(Constants.Conventions.Content.UrlName, culture) ?? string.Empty).Trim(); + source = (content.GetValue(Constants.Conventions.Content.UrlName, culture, published: published) ?? string.Empty).Trim(); } if (string.IsNullOrWhiteSpace(source)) diff --git a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs index c7050050e1..6e9f1db326 100644 --- a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs @@ -20,6 +20,7 @@ public interface IUrlSegmentProvider /// URL per culture. /// string? GetUrlSegment(IContentBase content, string? culture = null); + string? GetUrlSegment(IContentBase content, bool published, string? culture = null) => GetUrlSegment(content, culture); // TODO: For the 301 tracking, we need to add another extended interface to this so that // the RedirectTrackingEventHandler can ask the IUrlSegmentProvider if the URL is changing. diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 757e103727..37e3c6063c 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -33,6 +33,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddMultipleUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs index b791babe61..a6aaedca4c 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs @@ -1,5 +1,7 @@ using Examine; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.PropertyEditors; @@ -27,7 +29,10 @@ public class ContentValueSetBuilder : BaseValueSetBuilder, IContentVal private readonly ILocalizationService _localizationService; private readonly IContentTypeService _contentTypeService; private readonly ILogger _logger; + private readonly IDocumentUrlService _documentUrlService; + private readonly ILanguageService _languageService; + [Obsolete("Use the non-obsolete constructor. This will be removed in Umbraco 16.")] public ContentValueSetBuilder( PropertyEditorCollection propertyEditors, UrlSegmentProviderCollection urlSegmentProviders, @@ -38,6 +43,36 @@ public class ContentValueSetBuilder : BaseValueSetBuilder, IContentVal ILocalizationService localizationService, IContentTypeService contentTypeService, ILogger logger) + : this( + propertyEditors, + urlSegmentProviders, + userService, + shortStringHelper, + scopeProvider, + publishedValuesOnly, + localizationService, + contentTypeService, + logger, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService() + ) + { + + } + + [Obsolete("Use the non-obsolete constructor. This will be removed in Umbraco 16.")] + public ContentValueSetBuilder( + PropertyEditorCollection propertyEditors, + UrlSegmentProviderCollection urlSegmentProviders, + IUserService userService, + IShortStringHelper shortStringHelper, + ICoreScopeProvider scopeProvider, + bool publishedValuesOnly, + ILocalizationService localizationService, + IContentTypeService contentTypeService, + ILogger logger, + IDocumentUrlService documentUrlService, + ILanguageService languageService) : base(propertyEditors, publishedValuesOnly) { _urlSegmentProviders = urlSegmentProviders; @@ -47,8 +82,9 @@ public class ContentValueSetBuilder : BaseValueSetBuilder, IContentVal _localizationService = localizationService; _contentTypeService = contentTypeService; _logger = logger; + _documentUrlService = documentUrlService; + _languageService = languageService; } - /// public override IEnumerable GetValueSets(params IContent[] content) { @@ -73,6 +109,7 @@ public class ContentValueSetBuilder : BaseValueSetBuilder, IContentVal { IDictionary contentTypeDictionary = _contentTypeService.GetAll().ToDictionary(x => x.Key); + var defaultCulture = _languageService.GetDefaultIsoCodeAsync().GetAwaiter().GetResult(); // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` @@ -81,7 +118,7 @@ public class ContentValueSetBuilder : BaseValueSetBuilder, IContentVal { var isVariant = c.ContentType.VariesByCulture(); - var urlValue = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); // Always add invariant urlName + var urlValue = _documentUrlService.GetUrlSegment(c.Key, defaultCulture, false); // Always add invariant urlName var values = new Dictionary> { { "icon", c.ContentType.Icon?.Yield() ?? Enumerable.Empty() }, diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index b118b7f84c..5e634d9684 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -71,6 +71,7 @@ public class DatabaseSchemaCreator typeof(UserStartNodeDto), typeof(ContentNuDto), typeof(DocumentVersionDto), + typeof(DocumentUrlDto), typeof(KeyValueDto), typeof(UserLoginDto), typeof(ConsentDto), diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 0dda6bf003..d318b5f7e1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -99,5 +99,6 @@ public class UmbracoPlan : MigrationPlan // To 15.0.0 To("{7F4F31D8-DD71-4F0D-93FC-2690A924D84B}"); To("{1A8835EF-F8AB-4472-B4D8-D75B7C164022}"); + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs index a8131a0da4..42a4db8677 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs @@ -57,5 +57,8 @@ public class UmbracoPremigrationPlan : MigrationPlan To("{7BCB5352-B2ED-4D4B-B27D-ECDED930B50A}"); To("{3E69BF9B-BEAB-41B1-BB11-15383CCA1C7F}"); To("{F12C609B-86B9-4386-AFA4-78E02857247C}"); + + // To 15.0.0 + To("{B9133686-B758-404D-AF12-708AA80C7E44}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddDocumentUrl.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddDocumentUrl.cs new file mode 100644 index 0000000000..84c158eb33 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddDocumentUrl.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; + +[Obsolete("Remove in Umbraco 18.")] +public class AddDocumentUrl : MigrationBase +{ + private readonly IDocumentUrlService _documentUrlService; + + public AddDocumentUrl(IMigrationContext context, IDocumentUrlService documentUrlService) + : base(context) + { + _documentUrlService = documentUrlService; + } + + protected override void Migrate() + { + Create.Table().Do(); + _documentUrlService.InitAsync(false, CancellationToken.None); + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs index 46697b9c97..3c1d483d58 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs @@ -8,4 +8,5 @@ public enum IndexTypes Clustered, NonClustered, UniqueNonClustered, + UniqueClustered, } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentUrlDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentUrlDto.cs new file mode 100644 index 0000000000..4a006f9307 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentUrlDto.cs @@ -0,0 +1,41 @@ + +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +public class DocumentUrlDto +{ + public const string TableName = Constants.DatabaseSchema.Tables.DocumentUrl; + + [Column("id")] + [PrimaryKeyColumn(Clustered = false, AutoIncrement = true)] + public int NodeId { get; set; } + + [Index(IndexTypes.UniqueClustered, ForColumns = "uniqueId, languageId, isDraft", Name = "IX_" + TableName)] + [Column("uniqueId")] + [ForeignKey(typeof(NodeDto), Column = "uniqueId")] + public Guid UniqueId { get; set; } + + [Column("isDraft")] + public bool IsDraft { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + public int LanguageId { get; set; } + + // + // [Column("segment")] + // [NullSetting(NullSetting = NullSettings.Null)] + // [Length(PropertyDataDto.SegmentLength)] + // public string Segment { get; set; } = string.Empty; + + [Column("urlSegment")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string UrlSegment { get; set; } = string.Empty; + +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index ce9c659720..7579d66d82 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -742,7 +742,10 @@ public class DocumentRepository : ContentRepositoryBase _scopeAccessor = scopeAccessor; + + private IUmbracoDatabase Database + { + get + { + if (_scopeAccessor.AmbientScope is null) + { + throw new NotSupportedException("Need to be executed in a scope"); + } + + return _scopeAccessor.AmbientScope.Database; + } + } + + public void Save(IEnumerable publishedDocumentUrlSegments) + { + //TODO avoid this is called as first thing on first restart after install + IEnumerable documentKeys = publishedDocumentUrlSegments.Select(x => x.DocumentKey).Distinct(); + + Dictionary<(Guid UniqueId, int LanguageId, bool isDraft), DocumentUrlDto> dtoDictionary = publishedDocumentUrlSegments.Select(BuildDto).ToDictionary(x=> (x.UniqueId, x.LanguageId, x.IsDraft)); + + var toUpdate = new List(); + var toDelete = new List(); + var toInsert = dtoDictionary.Values.ToDictionary(x => (x.UniqueId, x.LanguageId, x.IsDraft)); + + foreach (IEnumerable group in documentKeys.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + Sql sql = Database.SqlContext.Sql() + .Select() + .From() + .Where(x => group.Contains(x.UniqueId)) + .ForUpdate(); + + List existingUrlsInBatch = Database.Fetch(sql); + + foreach (DocumentUrlDto existing in existingUrlsInBatch) + { + + if (dtoDictionary.TryGetValue((existing.UniqueId, existing.LanguageId, existing.IsDraft), out DocumentUrlDto? found)) + { + found.NodeId = existing.NodeId; + + // Only update if the url segment is different + if (found.UrlSegment != existing.UrlSegment) + { + toUpdate.Add(found); + } + // if we found it, we know we should not insert it as a new + toInsert.Remove((found.UniqueId, found.LanguageId, found.IsDraft)); + } + else + { + toDelete.Add(existing.NodeId); + } + } + } + + // do the deletes, updates and inserts + if (toDelete.Count > 0) + { + Database.DeleteMany().Where(x => toDelete.Contains(x.NodeId)).Execute(); + } + + if (toUpdate.Any()) + { + foreach (DocumentUrlDto updated in toUpdate) + { + Database.Update(updated); + } + } + + Database.InsertBulk(toInsert.Values); + } + + public IEnumerable GetAll() + { + List? dtos = Database.Fetch(Database.SqlContext.Sql().Select().From()); + + return dtos.Select(BuildModel); + } + + public void DeleteByDocumentKey(IEnumerable documentKeys) + { + foreach (IEnumerable group in documentKeys.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + Database.Execute(Database.SqlContext.Sql().Delete().WhereIn(x => x.UniqueId, group)); + } + } + + private PublishedDocumentUrlSegment BuildModel(DocumentUrlDto dto) => + new() + { + UrlSegment = dto.UrlSegment, + DocumentKey = dto.UniqueId, + LanguageId = dto.LanguageId, + IsDraft = dto.IsDraft + }; + + private DocumentUrlDto BuildDto(PublishedDocumentUrlSegment model) + { + return new DocumentUrlDto() + { + UrlSegment = model.UrlSegment, + UniqueId = model.DocumentKey, + LanguageId = model.LanguageId, + IsDraft = model.IsDraft, + }; + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 0a3cda3b6d..fe33fbe7b6 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -138,20 +138,23 @@ public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider public virtual string GetIndexType(IndexTypes indexTypes) { - string indexType; + var indexType = string.Empty; - if (indexTypes == IndexTypes.Clustered) + if (indexTypes == IndexTypes.UniqueClustered || indexTypes == IndexTypes.UniqueNonClustered) { - indexType = "CLUSTERED"; + indexType += " UNIQUE"; + } + + if (indexTypes == IndexTypes.UniqueClustered || indexTypes == IndexTypes.Clustered) + { + indexType += " CLUSTERED"; } else { - indexType = indexTypes == IndexTypes.NonClustered - ? "NONCLUSTERED" - : "UNIQUE NONCLUSTERED"; + indexType += " NONCLUSTERED"; } - return indexType; + return indexType.Trim(); } public virtual string GetSpecialDbType(SpecialDbType dbType) diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DomainCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DomainCacheService.cs index b4cb3019af..9efc749d6b 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/DomainCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DomainCacheService.cs @@ -37,7 +37,7 @@ public class DomainCacheService : IDomainCacheService { // probably this could be optimized with an index // but then we'd need a custom DomainStore of some sort - IEnumerable list = _domains.Select(x => x.Value).Where(x => x.ContentId == documentId); + IEnumerable list = _domains.Values.Where(x => x.ContentId == documentId); if (includeWildcards == false) { list = list.Where(x => x.IsWildcard == false); diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index e2782af371..4729d3baa7 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit e2782af3719ea1715e3f995fc9b48e04ce63774f +Subproject commit 4729d3baa7611ed63380abcfc184c1bb5a48b3bb diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 43ade570e9..ec4d0e2600 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -21,7 +21,7 @@ using Umbraco.Cms.Persistence.SqlServer; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Integration.DependencyInjection; using Umbraco.Cms.Tests.Integration.Extensions; -using Umbraco.Cms.Tests.Integration.TestServerTest; + using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Tests.Integration.Testing; @@ -162,6 +162,7 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase .AddExamine() .AddUmbracoSqlServerSupport() .AddUmbracoSqliteSupport() + .AddUmbracoHybridCache() .AddTestServices(TestHelper); if (TestOptions.Mapper) @@ -171,6 +172,7 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase .AddCoreMappingProfiles(); } + services.RemoveAll(x=>x.ImplementationType == typeof(DocumentUrlServiceInitializer)); services.AddSignalR(); services.AddMvc(); diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs index 74122d2514..2ff15c3539 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs @@ -11,6 +11,12 @@ namespace Umbraco.Cms.Tests.Integration.Testing; public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest { + protected const string TextpageKey = "B58B3AD4-62C2-4E27-B1BE-837BD7C533E0"; + protected const string SubPageKey = "07EABF4A-5C62-4662-9F2A-15BBB488BCA5"; + protected const string SubPage2Key = "0EED78FC-A6A8-4587-AB18-D3AFE212B1C4"; + protected const string SubPage3Key = "29BBB8CF-E69B-4A21-9363-02ED5B6637C4"; + protected const string TrashedKey = "EAE9EE57-FFE4-4841-8586-1B636C43A3D4"; + protected IContentTypeService ContentTypeService => GetRequiredService(); protected IFileService FileService => GetRequiredService(); @@ -44,26 +50,30 @@ public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest ContentTypeService.Save(ContentType); // Create and Save Content "Homepage" based on "umbTextpage" -> 1053 - Textpage = ContentBuilder.CreateSimpleContent(ContentType); - Textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0"); + Textpage = ContentBuilder.CreateSimpleContent(ContentType, "Textpage"); + Textpage.Key = new Guid(TextpageKey); ContentService.Save(Textpage, -1); // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054 Subpage = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 1", Textpage.Id); + Subpage.Key = new Guid(SubPageKey); var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); ContentService.Save(Subpage, -1, contentSchedule); // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1055 Subpage2 = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 2", Textpage.Id); + Subpage2.Key = new Guid(SubPage2Key); ContentService.Save(Subpage2, -1); Subpage3 = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 3", Textpage.Id); + Subpage3.Key = new Guid(SubPage3Key); ContentService.Save(Subpage3, -1); // Create and Save Content "Text Page Deleted" based on "umbTextpage" -> 1056 Trashed = ContentBuilder.CreateSimpleContent(ContentType, "Text Page Deleted", -20); Trashed.Trashed = true; + Trashed.Key = new Guid(TrashedKey); ContentService.Save(Trashed, -1); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index 646fbbd51c..8e4b836fee 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -1148,7 +1148,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent Assert.AreEqual("foo", entity.Name); var e = ContentService.GetById(entity.Id); - Assert.AreEqual("Home", e.Name); + Assert.AreEqual("Textpage", e.Name); savingWasCalled = true; }; @@ -1165,7 +1165,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent try { var content = ContentService.GetById(Textpage.Id); - Assert.AreEqual("Home", content.Name); + Assert.AreEqual("Textpage", content.Name); content.Name = "foo"; ContentService.Save(content); @@ -2186,7 +2186,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent { // Arrange var temp = ContentService.GetById(Textpage.Id); - Assert.AreEqual("Home", temp.Name); + Assert.AreEqual("Textpage", temp.Name); Assert.AreEqual(3, ContentService.CountChildren(temp.Id)); // Act @@ -2211,7 +2211,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent { // Arrange var temp = ContentService.GetById(Textpage.Id); - Assert.AreEqual("Home", temp.Name); + Assert.AreEqual("Textpage", temp.Name); Assert.AreEqual(3, ContentService.CountChildren(temp.Id)); // Act diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs new file mode 100644 index 0000000000..d4b02a8755 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs @@ -0,0 +1,190 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Handlers; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.DependencyInjection; +using Umbraco.Cms.Infrastructure.Examine; +using Umbraco.Cms.Infrastructure.Examine.DependencyInjection; +using Umbraco.Cms.Infrastructure.HostedServices; +using Umbraco.Cms.Infrastructure.Search; +using Umbraco.Cms.Tests.Common.Attributes; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping; +using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Mock)] +public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent +{ + protected IDocumentUrlService DocumentUrlService => GetRequiredService(); + protected ILanguageService LanguageService => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + builder.AddNotificationHandler(); + + builder.Services.AddHostedService(); + + } + + + + // + // [Test] + // [LongRunning] + // public async Task InitAsync() + // { + // // ContentService.PublishBranch(Textpage, true, []); + // // + // // for (int i = 3; i < 10; i++) + // // { + // // var unusedSubPage = ContentBuilder.CreateSimpleContent(ContentType, "Text Page " + i, Textpage.Id); + // // unusedSubPage.Key = Guid.NewGuid(); + // // ContentService.Save(unusedSubPage); + // // ContentService.Publish(unusedSubPage, new string[0]); + // // } + // // + // // await DocumentUrlService.InitAsync(CancellationToken.None); + // + // } + + [Test] + public async Task Trashed_documents_do_not_have_a_url_segment() + { + var isoCode = (await LanguageService.GetDefaultLanguageAsync()).IsoCode; + + var actual = DocumentUrlService.GetUrlSegment(Trashed.Key, isoCode, true); + + Assert.IsNull(actual); + } + + //TODO test with the urlsegment property value! + + [Test] + public async Task Deleted_documents_do_not_have_a_url_segment__Parent_deleted() + { + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + + ContentService.Delete(Textpage); + + var isoCode = (await LanguageService.GetDefaultLanguageAsync()).IsoCode; + + var actual = DocumentUrlService.GetUrlSegment(Subpage2.Key, isoCode, false); + + Assert.IsNull(actual); + } + + [Test] + public async Task Deleted_documents_do_not_have_a_url_segment() + { + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + + ContentService.Delete(Subpage2); + + var isoCode = (await LanguageService.GetDefaultLanguageAsync()).IsoCode; + + var actual = DocumentUrlService.GetUrlSegment(Subpage2.Key, isoCode, false); + + Assert.IsNull(actual); + } + + [Test] + [TestCase("/", "en-US", true, ExpectedResult = TextpageKey)] + [TestCase("/text-page-1", "en-US", true, ExpectedResult = SubPageKey)] + [TestCase("/text-page-2", "en-US", true, ExpectedResult = SubPage2Key)] + [TestCase("/text-page-3", "en-US", true, ExpectedResult = SubPage3Key)] + [TestCase("/", "en-US", false, ExpectedResult = TextpageKey)] + [TestCase("/text-page-1", "en-US", false, ExpectedResult = SubPageKey)] + [TestCase("/text-page-2", "en-US", false, ExpectedResult = SubPage2Key)] + [TestCase("/text-page-3", "en-US", false, ExpectedResult = SubPage3Key)] + public string? Expected_Routes(string route, string isoCode, bool loadDraft) + { + if (loadDraft is false) + { + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + } + + return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); + } + + [Test] + public void No_Published_Route_when_not_published() + { + Assert.IsNull(DocumentUrlService.GetDocumentKeyByRoute("/text-page-1", "en-US", null, false)); + } + + + [Test] + [TestCase("/text-page-1/sub-page-1", "en-US", true, ExpectedResult = "DF49F477-12F2-4E33-8563-91A7CC1DCDBB")] + [TestCase("/text-page-1/sub-page-1", "en-US", false, ExpectedResult = "DF49F477-12F2-4E33-8563-91A7CC1DCDBB")] + public string? Expected_Routes_with_subpages(string route, string isoCode, bool loadDraft) + { + // Create a subpage + var subsubpage = ContentBuilder.CreateSimpleContent(ContentType, "Sub Page 1", Subpage.Id); + subsubpage.Key = new Guid("DF49F477-12F2-4E33-8563-91A7CC1DCDBB"); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(subsubpage, -1, contentSchedule); + + if (loadDraft is false) + { + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + } + + return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); + } + + [Test] + [TestCase("/second-root", "en-US", true, ExpectedResult = "8E21BCD4-02CA-483D-84B0-1FC92702E198")] + [TestCase("/second-root", "en-US", false, ExpectedResult = "8E21BCD4-02CA-483D-84B0-1FC92702E198")] + public string? Second_root_cannot_hide_url(string route, string isoCode, bool loadDraft) + { + // Create a second root + var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); + secondRoot.Key = new Guid("8E21BCD4-02CA-483D-84B0-1FC92702E198"); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(secondRoot, -1, contentSchedule); + + if (loadDraft is false) + { + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + } + + return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); + } + + [Test] + [TestCase("/child-of-second-root", "en-US", true, ExpectedResult = "FF6654FB-BC68-4A65-8C6C-135567F50BD6")] + [TestCase("/child-of-second-root", "en-US", false, ExpectedResult = "FF6654FB-BC68-4A65-8C6C-135567F50BD6")] + public string? Child_of_second_root_do_not_have_parents_url_as_prefix(string route, string isoCode, bool loadDraft) + { + // Create a second root + var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(secondRoot, -1, contentSchedule); + + // Create a child of second root + var childOfSecondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Child of Second Root", secondRoot); + childOfSecondRoot.Key = new Guid("FF6654FB-BC68-4A65-8C6C-135567F50BD6"); + ContentService.Save(childOfSecondRoot, -1, contentSchedule); + + // Publish both the main root and the second root with descendants + if (loadDraft is false) + { + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + } + + return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs new file mode 100644 index 0000000000..c38bd544bc --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs @@ -0,0 +1,117 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Handlers; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Tests.Common.Attributes; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)] +public class DocumentUrlServiceTest_HideTopLevel_False : UmbracoIntegrationTestWithContent +{ + protected IDocumentUrlService DocumentUrlService => GetRequiredService(); + protected ILanguageService LanguageService => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.Configure(x => x.HideTopLevelNodeFromPath = false); + + builder.Services.AddUnique(); + builder.AddNotificationHandler(); + + builder.Services.AddHostedService(); + } + [Test] + [TestCase("/textpage/", "en-US", true, ExpectedResult = TextpageKey)] + [TestCase("/textpage/text-page-1", "en-US", true, ExpectedResult = SubPageKey)] + [TestCase("/textpage/text-page-2", "en-US", true, ExpectedResult = SubPage2Key)] + [TestCase("/textpage/text-page-3", "en-US", true, ExpectedResult = SubPage3Key)] + [TestCase("/textpage/", "en-US", false, ExpectedResult = TextpageKey)] + [TestCase("/textpage/text-page-1", "en-US", false, ExpectedResult = SubPageKey)] + [TestCase("/textpage/text-page-2", "en-US", false, ExpectedResult = SubPage2Key)] + [TestCase("/textpage/text-page-3", "en-US", false, ExpectedResult = SubPage3Key)] + public string? Expected_Routes(string route, string isoCode, bool loadDraft) + { + if (loadDraft is false) + { + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + } + + + return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); + } + + [Test] + [TestCase("/textpage/text-page-1/sub-page-1", "en-US", true, ExpectedResult = "DF49F477-12F2-4E33-8563-91A7CC1DCDBB")] + [TestCase("/textpage/text-page-1/sub-page-1", "en-US", false, ExpectedResult = "DF49F477-12F2-4E33-8563-91A7CC1DCDBB")] + public string? Expected_Routes_with_subpages(string route, string isoCode, bool loadDraft) + { + // Create a subpage + var subsubpage = ContentBuilder.CreateSimpleContent(ContentType, "Sub Page 1", Subpage.Id); + subsubpage.Key = new Guid("DF49F477-12F2-4E33-8563-91A7CC1DCDBB"); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(subsubpage, -1, contentSchedule); + + if (loadDraft is false) + { + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + } + + return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); + } + + [Test] + [TestCase("/second-root", "en-US", true, ExpectedResult = "8E21BCD4-02CA-483D-84B0-1FC92702E198")] + [TestCase("/second-root", "en-US", false, ExpectedResult = "8E21BCD4-02CA-483D-84B0-1FC92702E198")] + public string? Second_root_cannot_hide_url(string route, string isoCode, bool loadDraft) + { + // Create a second root + var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); + secondRoot.Key = new Guid("8E21BCD4-02CA-483D-84B0-1FC92702E198"); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(secondRoot, -1, contentSchedule); + + if (loadDraft is false) + { + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + } + + return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); + } + + [Test] + [TestCase("/second-root/child-of-second-root", "en-US", true, ExpectedResult = "FF6654FB-BC68-4A65-8C6C-135567F50BD6")] + [TestCase("/second-root/child-of-second-root", "en-US", false, ExpectedResult = "FF6654FB-BC68-4A65-8C6C-135567F50BD6")] + public string? Child_of_second_root_do_not_have_parents_url_as_prefix(string route, string isoCode, bool loadDraft) + { + // Create a second root + var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(secondRoot, -1, contentSchedule); + + // Create a child of second root + var childOfSecondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Child of Second Root", secondRoot); + childOfSecondRoot.Key = new Guid("FF6654FB-BC68-4A65-8C6C-135567F50BD6"); + ContentService.Save(childOfSecondRoot, -1, contentSchedule); + + // Publish both the main root and the second root with descendants + if (loadDraft is false) + { + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + } + + return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/BackOfficeExamineSearcherTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/BackOfficeExamineSearcherTests.cs index 2bc60dcd59..fd852afa86 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/BackOfficeExamineSearcherTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/BackOfficeExamineSearcherTests.cs @@ -19,6 +19,7 @@ using Umbraco.Cms.Infrastructure.Search; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping; using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; using Umbraco.Cms.Web.Common.Security; @@ -64,11 +65,8 @@ public class BackOfficeExamineSearcherTests : ExamineBaseTest protected override void CustomTestSetup(IUmbracoBuilder builder) { + base.CustomTestSetup(builder); builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder - .AddNotificationHandler(); builder.AddNotificationHandler(); builder.AddExamineIndexes(); builder.Services.AddHostedService(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineBaseTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineBaseTest.cs index fa14bcf18f..66e9ad85fc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineBaseTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineBaseTest.cs @@ -5,15 +5,19 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine; @@ -33,6 +37,16 @@ public abstract class ExamineBaseTest : UmbracoIntegrationTest protected override void ConfigureTestServices(IServiceCollection services) => services.AddSingleton(); + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + base.CustomTestSetup(builder); + builder.Services.AddUnique(); + builder + .AddNotificationHandler(); + builder.Services.AddHostedService(); + + } /// /// Used to create and manage a testable index /// diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineExternalIndexTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineExternalIndexTests.cs index a4258755e8..24c04b643d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineExternalIndexTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineExternalIndexTests.cs @@ -48,6 +48,7 @@ public class ExamineExternalIndexTests : ExamineBaseTest Services.DisposeIfDisposable(); } + private IExamineExternalIndexSearcherTest ExamineExternalIndexSearcher => GetRequiredService(); @@ -66,11 +67,8 @@ public class ExamineExternalIndexTests : ExamineBaseTest protected override void CustomTestSetup(IUmbracoBuilder builder) { + base.CustomTestSetup(builder); builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder - .AddNotificationHandler(); builder.AddNotificationHandler(); builder.AddExamineIndexes(); builder.Services.AddHostedService(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs index 679daeca75..2b25fde0a3 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs @@ -1,17 +1,21 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping; namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.UrlAndDomains; @@ -69,6 +73,12 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest builder.Services.AddUnique(_variationContextAccessor); builder.AddUmbracoHybridCache(); builder.AddNuCache(); + + // Ensure cache refreshers runs + builder.Services.AddUnique(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + } private readonly TestVariationContextAccessor _variationContextAccessor = new(); diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs index 54b6abea9a..b4d315a699 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs @@ -117,20 +117,25 @@ public class PublishedSnapshotServiceTestBase protected static PublishedRouter CreatePublishedRouter( IUmbracoContextAccessor umbracoContextAccessor, IEnumerable contentFinders = null, - IPublishedUrlProvider publishedUrlProvider = null) => new( - Mock.Of>(x => x.CurrentValue == new WebRoutingSettings()), - new ContentFinderCollection(() => contentFinders ?? Enumerable.Empty()), - new TestLastChanceFinder(), - new TestVariationContextAccessor(), - Mock.Of(), - Mock.Of>(), - publishedUrlProvider ?? Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - umbracoContextAccessor, - Mock.Of()); + IPublishedUrlProvider publishedUrlProvider = null, + IDomainCache domainCache = null) + { + return new( + Mock.Of>(x => x.CurrentValue == new WebRoutingSettings()), + new ContentFinderCollection(() => contentFinders ?? Enumerable.Empty()), + new TestLastChanceFinder(), + new TestVariationContextAccessor(), + Mock.Of(), + Mock.Of>(), + publishedUrlProvider ?? Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + umbracoContextAccessor, + Mock.Of(), + domainCache ?? Mock.Of()); + } protected IUmbracoContextAccessor GetUmbracoContextAccessor(string urlAsString) { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedContentCacheTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedContentCacheTests.cs index 9ccb285e53..b3f0abe5e1 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedContentCacheTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedContentCacheTests.cs @@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -16,6 +17,8 @@ public class PublishedContentCacheTests : DeliveryApiTests private readonly Guid _contentTwoId = Guid.Parse("4EF11E1E-FB50-4627-8A86-E10ED6F4DCE4"); private IPublishedSnapshotAccessor _publishedSnapshotAccessor = null!; + private IPublishedContentCache _contentCacheMock; + private IDocumentUrlService _documentUrlService; [SetUp] public void Setup() @@ -30,6 +33,14 @@ public class PublishedContentCacheTests : DeliveryApiTests var contentTwoMock = new Mock(); ConfigurePublishedContentMock(contentTwoMock, _contentTwoId, "Content Two", "content-two", contentTypeTwoMock.Object, Array.Empty()); + var documentUrlService = new Mock(); + documentUrlService + .Setup(x => x.GetDocumentKeyByRoute("content-one", It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_contentOneId); + documentUrlService + .Setup(x => x.GetDocumentKeyByRoute("content-two", It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_contentTwoId); + var contentCacheMock = new Mock(); contentCacheMock .Setup(m => m.GetByRoute(It.IsAny(), "content-one", null, null)) @@ -52,12 +63,14 @@ public class PublishedContentCacheTests : DeliveryApiTests publishedSnapshotAccessorMock.Setup(m => m.TryGetPublishedSnapshot(out publishedSnapshot)).Returns(true); _publishedSnapshotAccessor = publishedSnapshotAccessorMock.Object; + _contentCacheMock = contentCacheMock.Object; + _documentUrlService = documentUrlService.Object; } [Test] public void PublishedContentCache_CanGetById() { - var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings()); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(), _documentUrlService, _contentCacheMock); var content = publishedContentCache.GetById(_contentOneId); Assert.IsNotNull(content); Assert.AreEqual(_contentOneId, content.Key); @@ -68,7 +81,7 @@ public class PublishedContentCacheTests : DeliveryApiTests [Test] public void PublishedContentCache_CanGetByRoute() { - var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings()); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(), _documentUrlService, _contentCacheMock); var content = publishedContentCache.GetByRoute("content-two"); Assert.IsNotNull(content); Assert.AreEqual(_contentTwoId, content.Key); @@ -79,7 +92,7 @@ public class PublishedContentCacheTests : DeliveryApiTests [Test] public void PublishedContentCache_CanGetByIds() { - var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings()); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(), _documentUrlService, _contentCacheMock); var content = publishedContentCache.GetByIds(new[] { _contentOneId, _contentTwoId }).ToArray(); Assert.AreEqual(2, content.Length); Assert.AreEqual(_contentOneId, content.First().Key); @@ -91,7 +104,7 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_GetById_SupportsDenyList(bool denied) { var denyList = denied ? new[] { "theOtherContentType" } : null; - var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList)); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); var content = publishedContentCache.GetById(_contentTwoId); if (denied) @@ -109,7 +122,7 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_GetByRoute_SupportsDenyList(bool denied) { var denyList = denied ? new[] { "theContentType" } : null; - var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList)); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); var content = publishedContentCache.GetByRoute("content-one"); if (denied) @@ -127,7 +140,7 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_GetByIds_SupportsDenyList(string deniedContentType) { var denyList = new[] { deniedContentType }; - var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList)); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); var content = publishedContentCache.GetByIds(new[] { _contentOneId, _contentTwoId }).ToArray(); Assert.AreEqual(1, content.Length); @@ -145,7 +158,7 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_GetById_CanRetrieveContentTypesOutsideTheDenyList() { var denyList = new[] { "theContentType" }; - var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList)); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); var content = publishedContentCache.GetById(_contentTwoId); Assert.IsNotNull(content); Assert.AreEqual(_contentTwoId, content.Key); @@ -157,7 +170,7 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_GetByRoute_CanRetrieveContentTypesOutsideTheDenyList() { var denyList = new[] { "theOtherContentType" }; - var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList)); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); var content = publishedContentCache.GetByRoute("content-one"); Assert.IsNotNull(content); Assert.AreEqual(_contentOneId, content.Key); @@ -169,7 +182,7 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_GetByIds_CanDenyAllRequestedContent() { var denyList = new[] { "theContentType", "theOtherContentType" }; - var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList)); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); var content = publishedContentCache.GetByIds(new[] { _contentOneId, _contentTwoId }).ToArray(); Assert.IsEmpty(content); } @@ -178,11 +191,18 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_DenyListIsCaseInsensitive() { var denyList = new[] { "THEcontentTYPE" }; - var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList)); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); var content = publishedContentCache.GetByRoute("content-one"); Assert.IsNull(content); } + private IRequestCultureService CreateRequestCultureService() + { + var mock = new Mock(); + + return mock.Object; + } + private IRequestPreviewService CreateRequestPreviewService(bool isPreview = false) { var previewServiceMock = new Mock(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs index 3945e2346d..9f0e2cbbf4 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs @@ -305,7 +305,7 @@ public class DomainsAndCulturesTests : UrlRoutingTestBase GlobalSettings.HideTopLevelNodeFromPath = false; var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); + var publishedRouter = CreatePublishedRouter(umbracoContextAccessor, domainCache: umbracoContextAccessor.GetRequiredUmbracoContext().PublishedSnapshot.Domains); var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/PublishedRouterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/PublishedRouterTests.cs index da5c641b2a..b56e55ff67 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/PublishedRouterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/PublishedRouterTests.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; @@ -35,7 +36,9 @@ public class PublishedRouterTests Mock.Of(), Mock.Of(), umbracoContextAccessor, - Mock.Of()); + Mock.Of(), + Mock.Of() + ); private IUmbracoContextAccessor GetUmbracoContextAccessor() { From 349b102fbe9ec769f44f22e59527bb3f34a7a014 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 27 Sep 2024 12:15:15 +0200 Subject: [PATCH 02/25] Execute User data migrations as pre-migrations (#17140) --- .../Migrations/Upgrade/UmbracoPlan.cs | 5 ++--- .../Migrations/Upgrade/UmbracoPremigrationPlan.cs | 5 +++++ .../Migrations/Upgrade/V_14_0_0/MigrateTours.cs | 12 +++++++----- .../Migrations/Upgrade/V_15_0_0/AddKindToUser.cs | 13 +++++++------ 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index d318b5f7e1..649190f5f0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -78,7 +78,7 @@ public class UmbracoPlan : MigrationPlan To("{C567DE81-DF92-4B99-BEA8-CD34EF99DA5D}"); To("{0D82C836-96DD-480D-A924-7964E458BD34}"); To("{1A0FBC8A-6FC6-456C-805C-B94816B2E570}"); - To("{302DE171-6D83-4B6B-B3C0-AC8808A16CA1}"); + To("{302DE171-6D83-4B6B-B3C0-AC8808A16CA1}"); To("{8184E61D-ECBA-4AAA-B61B-D7A82EB82EB7}"); To("{E261BF01-2C7F-4544-BAE7-49D545B21D68}"); To("{5A2EF07D-37B4-49D5-8E9B-3ED01877263B}"); @@ -98,7 +98,6 @@ public class UmbracoPlan : MigrationPlan // To 15.0.0 To("{7F4F31D8-DD71-4F0D-93FC-2690A924D84B}"); - To("{1A8835EF-F8AB-4472-B4D8-D75B7C164022}"); - + To("{1A8835EF-F8AB-4472-B4D8-D75B7C164022}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs index 42a4db8677..4c52f8867c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs @@ -59,6 +59,11 @@ public class UmbracoPremigrationPlan : MigrationPlan To("{F12C609B-86B9-4386-AFA4-78E02857247C}"); // To 15.0.0 + // - The tours data migration was run as part of the regular upgrade plan for V14, but as it affects User data, + // we need it to be run before the V15 User data migrations run. In the regular upgrade plan it has now been + // replaced with a noop migration for the corresponding migration state. + To("{A08254B6-D9E7-4207-A496-2ED0A87FB4FD}"); + To("{69AA6889-8B67-42B4-AA4F-114704487A45}"); To("{B9133686-B758-404D-AF12-708AA80C7E44}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateTours.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateTours.cs index 17444c33d7..934fa75914 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateTours.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateTours.cs @@ -26,16 +26,18 @@ internal class MigrateTours : UnscopedMigrationBase protected override void Migrate() { + // if the table already exists, do nothing + if (TableExists(Constants.DatabaseSchema.Tables.UserData)) + { + Context.Complete(); + return; + } + using IScope scope = _scopeProvider.CreateScope(); using IDisposable notificationSuppression = scope.Notifications.Suppress(); ScopeDatabase(scope); // create table - if (TableExists(Constants.DatabaseSchema.Tables.UserData)) - { - return; - } - Create.Table().Do(); // transform all existing UserTour fields in to userdata diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddKindToUser.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddKindToUser.cs index b807c4cc9a..5195f27391 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddKindToUser.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddKindToUser.cs @@ -19,6 +19,13 @@ public class AddKindToUser : UnscopedMigrationBase protected override void Migrate() { + // If the new column already exists we'll do nothing. + if (ColumnExists(Constants.DatabaseSchema.Tables.User, NewColumnName)) + { + Context.Complete(); + return; + } + InvalidateBackofficeUserAccess = true; using IScope scope = _scopeProvider.CreateScope(); @@ -48,12 +55,6 @@ public class AddKindToUser : UnscopedMigrationBase private void MigrateSqlite() { - // If the new column already exists we'll do nothing. - if (ColumnExists(Constants.DatabaseSchema.Tables.User, NewColumnName)) - { - return; - } - /* * We commit the initial transaction started by the scope. This is required in order to disable the foreign keys. * We then begin a new transaction, this transaction will be committed or rolled back by the scope, like normal. From 5c6fb4f6ed8a7619c804da3f321de6643c0542a8 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 27 Sep 2024 14:25:07 +0200 Subject: [PATCH 03/25] Fixed small routing issues, and implemented PublishedContent.Path in an obsolete way --- .../Services/DocumentUrlService.cs | 11 ++-- .../PublishedContent.cs | 52 +++++++++++++++++-- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs index bcc0cbed47..019a3ce13a 100644 --- a/src/Umbraco.Core/Services/DocumentUrlService.cs +++ b/src/Umbraco.Core/Services/DocumentUrlService.cs @@ -501,11 +501,12 @@ public class DocumentUrlService : IDocumentUrlService return domains.ToDictionary(x => x.LanguageIsoCode!); }); - var urlSegments = new List(); foreach (var culture in cultures) { + var urlSegments = new List(); IDomain? foundDomain = null; + var hasUrlInCulture = true; foreach (Guid ancestorOrSelfKey in ancestorsOrSelfKeysArray) { if (ancestorOrSelfKeyToDomains.TryGetValue(ancestorOrSelfKey, out Task>? domainDictionaryTask)) @@ -522,12 +523,16 @@ public class DocumentUrlService : IDocumentUrlService { urlSegments.Add(publishedDocumentUrlSegment.UrlSegment); } + else + { + hasUrlInCulture = false; + } } var isRootFirstItem = GetTopMostRootKey() == ancestorsOrSelfKeysArray.Last(); result.Add(new UrlInfo( text: GetFullUrl(isRootFirstItem, urlSegments, foundDomain), - isUrl: true, + isUrl: hasUrlInCulture, culture: culture )); @@ -543,7 +548,7 @@ public class DocumentUrlService : IDocumentUrlService if (foundDomain is not null) { - return foundDomain.DomainName + string.Join('/', urlSegments); + return foundDomain.DomainName.EnsureEndsWith("/") + string.Join('/', urlSegments); } return '/' + string.Join('/', urlSegments.Skip(_globalSettings.HideTopLevelNodeFromPath && isRootFirstItem ? 1 : 0)); diff --git a/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs b/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs index 21bb651d59..645f23b12d 100644 --- a/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs +++ b/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs @@ -1,5 +1,11 @@ -using Umbraco.Cms.Core.Exceptions; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Exceptions; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.HybridCache; @@ -66,8 +72,48 @@ internal class PublishedContent : PublishedContentBase public override int SortOrder { get; } - // TODO: Remove path. - public override string Path => string.Empty; + [Obsolete] + public override string Path + { + get + { + var documentNavigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); + var idKeyMap = StaticServiceProvider.Instance.GetRequiredService(); + + + if (documentNavigationQueryService.TryGetAncestorsOrSelfKeys(Key, out var ancestorsOrSelfKeys)) + { + var sb = new StringBuilder("-1"); + foreach (Guid ancestorsOrSelfKey in ancestorsOrSelfKeys.Reverse()) + { + var idAttempt = idKeyMap.GetIdForKey(ancestorsOrSelfKey, GetObjectType()); + if (idAttempt.Success) + { + sb.AppendFormat(",{0}", idAttempt.Result); + } + } + + return sb.ToString(); + } + + return string.Empty; + } + } + + private UmbracoObjectTypes GetObjectType() + { + switch (ItemType) + { + case PublishedItemType.Content: + return UmbracoObjectTypes.Document; + case PublishedItemType.Media: + return UmbracoObjectTypes.Media; + case PublishedItemType.Member: + return UmbracoObjectTypes.Member; + default: + return UmbracoObjectTypes.Unknown; + } + } public override int? TemplateId { get; } From 421ea1c55661d46c42011c0c7e63bffdd42e402e Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:28:21 +0200 Subject: [PATCH 04/25] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 4729d3baa7..3b8ec46d3a 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 4729d3baa7611ed63380abcfc184c1bb5a48b3bb +Subproject commit 3b8ec46d3aa9ccb09e476ac8767c500fec6d4e89 From 1fa132fb5f94d1e460cc56460250df04394d200e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 27 Sep 2024 20:52:03 +0200 Subject: [PATCH 05/25] Fixed issue with routing, that lead to invariant content underneath variant did not work correctly --- src/Umbraco.Core/Services/DocumentUrlService.cs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs index 019a3ce13a..2964314399 100644 --- a/src/Umbraco.Core/Services/DocumentUrlService.cs +++ b/src/Umbraco.Core/Services/DocumentUrlService.cs @@ -225,7 +225,7 @@ public class DocumentUrlService : IDocumentUrlService var toDelete = new List(); var allCultures = documents.SelectMany(x => x.AvailableCultures ).Distinct(); - var languages = await _languageService.GetMultipleAsync(allCultures); + var languages = await _languageService.GetAllAsync(); var languageDictionary = languages.ToDictionary(x=>x.IsoCode); foreach (IContent document in documents) @@ -235,20 +235,9 @@ public class DocumentUrlService : IDocumentUrlService _logger.LogTrace("Rebuilding urls for document with key {DocumentKey}", document.Key); } - if (document.AvailableCultures.Any()) + foreach ((string culture, ILanguage language) in languageDictionary) { - foreach (var culture in document.AvailableCultures) - { - var language = languageDictionary[culture]; - - HandleCaching(_coreScopeProvider.Context!, document, culture, language, toDelete, toSave); - } - } - else - { - var language = await _languageService.GetDefaultLanguageAsync(); - - HandleCaching(_coreScopeProvider.Context!, document, null, language!, toDelete, toSave); + HandleCaching(_coreScopeProvider.Context!, document, document.ContentType.VariesByCulture() ? culture : null, language, toDelete, toSave); } } From 1be503e71f5e01384c773b8e63a2e7a73cf6f7ee Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 30 Sep 2024 07:01:18 +0200 Subject: [PATCH 06/25] Block level variance (#17120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Block level variance - initial commit * Remove TODOs * Only convert RTEs with blocks * Fix JSON paths for block level property validation * Rename Properties to Values * Correct the JSON path of block level validation errors * Make it possible to skip content migration + ensure backwards compat for the new block format * Partial culture variance publishing at property level * UDI to key conversion for block editors - draft, WIP, do NOT merge 😄 (#16970) * Convert block UDIs to GUIDs * Fix merge * Fix merge issues * Rework nested layout item key parsing for backwards compatibility * Clean-up * Reverse block layout item key calculation * Review * Use IOptions to skip content migrations * Remove "published" from data editor feature naming, as it can be used in other contexts too * Parallel migration * Don't use deprecated constructor * Ensure that layout follows structure for partial publishing * Block Grid element level variance + tests (incl. refactor of element level variation tests) * Rollback unintended changes to Program.cs * Fix bad casing * Minor formatting * RTE element level variance + tests * Remove obsoleted constructors * Use Umbraco.RichText instead of Umbraco.TinyMCE as layout alias for blocks in the RTE * Fix bad merge * Temporary fix for new cache in integration tests * Add EditorAlias to block level properties * Remove the unintended PropertyEditorAlias output for block values * Add EditorAlias to Datatype Item model * Update OpenApi.json * Introduce "expose" for blocks * Strict (explicit) handling for Expose * Improve handling of document and element level variance changes * Refactor variance alignment for published rendering * Block UDI to Key conversion should also register as a conversion * Convert newly added RTE unit test to new RTE blocks format * Minor review changes * Run memory intensive tests on Linux only * Add tests proving that AllowEditInvariantFromNonDefault has effect for block level variance too * Fix the Platform annotations * Removed Platform annotations for tests. * Fix merge * Obsolete old PublishCulture extension * More fixing bad merge --------- Co-authored-by: Niels Lyngsø Co-authored-by: nikolajlauridsen --- .../Mapping/Item/ItemTypeMapDefinition.cs | 1 + src/Umbraco.Cms.Api.Management/OpenApi.json | 4 + .../Item/DataTypeItemResponseModel.cs | 2 + .../Models/Blocks/BlockEditorDataConverter.cs | 54 +- .../Blocks/BlockGridEditorDataConverter.cs | 10 +- .../Models/Blocks/BlockGridItem.cs | 59 +- .../Models/Blocks/BlockGridLayoutItem.cs | 27 +- .../Models/Blocks/BlockGridValue.cs | 3 + .../Models/Blocks/BlockItemData.cs | 51 +- .../Models/Blocks/BlockItemVariation.cs | 21 + .../Models/Blocks/BlockLayoutItemBase.cs | 84 + .../Blocks/BlockListEditorDataConverter.cs | 12 +- .../Models/Blocks/BlockListItem.cs | 50 +- .../Models/Blocks/BlockListLayoutItem.cs | 27 +- .../Models/Blocks/BlockListValue.cs | 3 + .../Models/Blocks/BlockPropertyValue.cs | 13 + src/Umbraco.Core/Models/Blocks/BlockValue.cs | 11 + .../Blocks/ContentAndSettingsReference.cs | 22 +- .../Models/Blocks/IBlockLayoutItem.cs | 6 + .../Models/Blocks/IBlockReference.cs | 2 +- .../Models/Blocks/RichTextBlockItem.cs | 44 +- .../Models/Blocks/RichTextBlockLayoutItem.cs | 27 +- .../Models/Blocks/RichTextBlockValue.cs | 10 +- .../RichTextEditorBlockDataConverter.cs | 8 +- .../Models/ContentRepositoryExtensions.cs | 36 +- src/Umbraco.Core/Models/IProperty.cs | 3 + src/Umbraco.Core/Models/Property.cs | 28 +- .../PropertyEditors/DataEditor.cs | 6 + .../PropertyEditors/IDataEditor.cs | 14 + src/Umbraco.Core/Services/ContentService.cs | 24 +- .../DeliveryApi/ApiRichTextElementParser.cs | 8 +- .../DeliveryApi/ApiRichTextMarkupParser.cs | 8 +- .../DeliveryApi/ApiRichTextParserBase.cs | 2 + .../UmbracoBuilder.CoreServices.cs | 1 + .../Migrations/Upgrade/UmbracoPlan.cs | 3 + .../ConvertBlockEditorPropertiesBase.cs | 281 +++ .../ConvertBlockEditorPropertiesOptions.cs | 29 + .../ConvertBlockGridEditorProperties.cs | 47 + .../ConvertBlockListEditorProperties.cs | 47 + .../ConvertRichTextEditorProperties.cs | 74 + ...ckEditorPropertyNotificationHandlerBase.cs | 44 +- .../BlockEditorPropertyValueEditor.cs | 77 +- .../BlockEditorValidatorBase.cs | 27 +- .../PropertyEditors/BlockEditorValues.cs | 32 +- .../BlockGridPropertyEditor.cs | 12 + .../BlockGridPropertyEditorBase.cs | 10 +- .../BlockListPropertyEditor.cs | 12 + .../BlockListPropertyEditorBase.cs | 8 +- .../BlockValuePropertyIndexValueFactory.cs | 6 +- .../BlockValuePropertyValueEditorBase.cs | 222 ++- .../MediaPicker3PropertyEditor.cs | 8 +- .../PropertyEditors/RichTextPropertyEditor.cs | 93 +- .../RichTextPropertyIndexValueFactory.cs | 4 +- .../ValueConverters/BlockEditorConverter.cs | 51 +- ...EditorPropertyValueConstructorCacheBase.cs | 6 +- .../BlockEditorVarianceHandler.cs | 137 ++ .../BlockGridPropertyValueConverter.cs | 27 +- .../BlockGridPropertyValueCreator.cs | 13 +- .../BlockListPropertyValueConverter.cs | 31 +- .../BlockListPropertyValueCreator.cs | 9 +- .../BlockPropertyValueConverterBase.cs | 255 --- .../BlockPropertyValueCreatorBase.cs | 61 +- .../RichTextBlockPropertyValueCreator.cs | 9 +- .../ValueConverters/RichTextParsingRegexes.cs | 2 +- .../RteBlockRenderingValueConverter.cs | 30 +- .../Serialization/JsonBlockValueConverter.cs | 20 +- .../Services/ContentServiceTests.cs | 4 +- .../Repositories/DocumentRepositoryTest.cs | 10 +- .../BlockEditorBackwardsCompatibilityTests.cs | 522 ++++++ .../BlockEditorElementVariationTestBase.cs | 172 ++ .../BlockGridElementLevelVariationTests.cs | 533 ++++++ ...kListElementLevelVariationTests.Parsing.cs | 662 +++++++ ...stElementLevelVariationTests.Publishing.cs | 1551 +++++++++++++++++ .../BlockListElementLevelVariationTests.cs | 167 ++ .../BlockListPropertyEditorTests.cs | 365 ++++ .../PropertyIndexValueFactoryTests.cs | 55 +- .../RichTextElementLevelVariationTests.cs | 495 ++++++ .../RichTextPropertyEditorTests.cs | 20 +- .../Services/ContentValidationServiceTests.cs | 141 +- .../Umbraco.Tests.Integration.csproj | 6 + .../DeliveryApi/RichTextParserTests.cs | 10 +- .../Umbraco.Core/Models/ContentTests.cs | 11 +- .../Umbraco.Core/Models/VariationTests.cs | 24 +- .../BlockEditorComponentTests.cs | 141 +- .../BlockGridPropertyValueConverterTests.cs | 59 +- .../BlockListPropertyValueConverterTests.cs | 122 +- .../BlockPropertyValueConverterTestsBase.cs | 4 + .../DataValueEditorReuseTests.cs | 5 +- .../RichTextPropertyEditorHelperTests.cs | 110 +- .../BlockEditorVarianceHandlerTests.cs | 75 + .../JsonBlockValueConverterTests.cs | 170 +- 91 files changed, 6765 insertions(+), 1037 deletions(-) create mode 100644 src/Umbraco.Core/Models/Blocks/BlockItemVariation.cs create mode 100644 src/Umbraco.Core/Models/Blocks/BlockLayoutItemBase.cs create mode 100644 src/Umbraco.Core/Models/Blocks/BlockPropertyValue.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesBase.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesOptions.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockGridEditorProperties.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockListEditorProperties.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertRichTextEditorProperties.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorVarianceHandler.cs delete mode 100644 src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueConverterBase.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorBackwardsCompatibilityTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockGridElementLevelVariationTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Parsing.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PropertyEditors/BlockEditorVarianceHandlerTests.cs diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs index 9152970b36..5b58862377 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs @@ -48,6 +48,7 @@ public class ItemTypeMapDefinition : IMapDefinition target.Name = source.Name ?? string.Empty; target.Id = source.Key; target.EditorUiAlias = source.EditorUiAlias; + target.EditorAlias = source.EditorAlias; target.IsDeletable = source.IsDeletableDataType(); } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 29acbdd16a..eb912bf856 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -35987,6 +35987,7 @@ }, "DataTypeItemResponseModel": { "required": [ + "editorAlias", "id", "isDeletable", "name" @@ -36004,6 +36005,9 @@ "type": "string", "nullable": true }, + "editorAlias": { + "type": "string" + }, "isDeletable": { "type": "boolean" } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/Item/DataTypeItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/Item/DataTypeItemResponseModel.cs index b753aa22b2..947abe388d 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/Item/DataTypeItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/Item/DataTypeItemResponseModel.cs @@ -6,5 +6,7 @@ public class DataTypeItemResponseModel : NamedItemResponseModelBase { public string? EditorUiAlias { get; set; } + public string EditorAlias { get; set; } = string.Empty; + public bool IsDeletable { get; set; } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs index e197f1d59e..5e263cf4c0 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs @@ -62,13 +62,63 @@ public abstract class BlockEditorDataConverter public BlockEditorData Convert(TValue? value) { - if (value?.GetLayouts() is not IEnumerable layouts) + if (value is not null) + { + var converted = ConvertOriginalBlockFormat(value.ContentData); + if (converted) + { + ConvertOriginalBlockFormat(value.SettingsData); + AmendExpose(value); + } + } + + TLayout[]? layouts = value?.GetLayouts()?.ToArray(); + if (layouts is null) { return BlockEditorData.Empty; } IEnumerable references = GetBlockReferences(layouts); - return new BlockEditorData(references, value); + return new BlockEditorData(references, value!); + } + + // this method is only meant to have any effect when migrating block editor values + // from the original format to the new, variant enabled format + private void AmendExpose(TValue value) + => value.Expose = value.ContentData.Select(cd => new BlockItemVariation(cd.Key, null, null)).ToList(); + + // this method is only meant to have any effect when migrating block editor values + // from the original format to the new, variant enabled format + private bool ConvertOriginalBlockFormat(List blockItemDatas) + { + var converted = false; + foreach (BlockItemData blockItemData in blockItemDatas) + { + // only overwrite the Properties collection if none have been added at this point + if (blockItemData.Values.Any() is false && blockItemData.RawPropertyValues.Any()) + { + blockItemData.Values = blockItemData + .RawPropertyValues + .Select(item => new BlockPropertyValue { Alias = item.Key, Value = item.Value }) + .ToList(); + converted = true; + } + + // no matter what, clear the RawPropertyValues collection so it is not saved back to the DB + blockItemData.RawPropertyValues.Clear(); + + // assign the correct Key if only a UDI is set + if (blockItemData.Key == Guid.Empty && blockItemData.Udi is GuidUdi guidUdi) + { + blockItemData.Key = guidUdi.Guid; + converted = true; + } + + // no matter what, clear the UDI value so it's not saved back to the DB + blockItemData.Udi = null; + } + + return converted; } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockGridEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockGridEditorDataConverter.cs index b771ed1e3c..b2e3a1337d 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockGridEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockGridEditorDataConverter.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Models.Blocks; @@ -12,12 +10,6 @@ namespace Umbraco.Cms.Core.Models.Blocks; /// public class BlockGridEditorDataConverter : BlockEditorDataConverter { - [Obsolete("Use the constructor that takes IJsonSerializer. Will be removed in V15.")] - public BlockGridEditorDataConverter() - : this(StaticServiceProvider.Instance.GetRequiredService()) - { - } - public BlockGridEditorDataConverter(IJsonSerializer jsonSerializer) : base(jsonSerializer) { @@ -27,7 +19,7 @@ public class BlockGridEditorDataConverter : BlockEditorDataConverter ExtractContentAndSettingsReferences(BlockGridLayoutItem item) { - var references = new List { new(item.ContentUdi, item.SettingsUdi) }; + var references = new List { new(item.ContentKey, item.SettingsKey) }; references.AddRange(item.Areas.SelectMany(area => area.Items.SelectMany(ExtractContentAndSettingsReferences))); return references; } diff --git a/src/Umbraco.Core/Models/Blocks/BlockGridItem.cs b/src/Umbraco.Core/Models/Blocks/BlockGridItem.cs index abe8cc89a0..8c463187e3 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockGridItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockGridItem.cs @@ -23,21 +23,45 @@ namespace Umbraco.Cms.Core.Models.Blocks /// contentUdi /// or /// content + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockGridItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) + : this( + (contentUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(contentUdi)), + content, + (settingsUdi as GuidUdi)?.Guid, + settings) { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + } + + public BlockGridItem(Guid contentKey, IPublishedElement content, Guid? settingsKey, IPublishedElement? settings) + { + ContentKey = contentKey; + ContentUdi = new GuidUdi(Constants.UdiEntityType.Element, contentKey); Content = content ?? throw new ArgumentNullException(nameof(content)); - SettingsUdi = settingsUdi; + SettingsKey = settingsKey; + SettingsUdi = settingsKey.HasValue + ? new GuidUdi(Constants.UdiEntityType.Element, settingsKey.Value) + : null; Settings = settings; } + /// + /// Gets the content key. + /// + public Guid ContentKey { get; set; } + + /// + /// Gets the settings key. + /// + public Guid? SettingsKey { get; set; } + /// /// Gets the content UDI. /// /// /// The content UDI. /// - [DataMember(Name = "contentUdi")] + [Obsolete("Use ContentKey instead. Will be removed in V18.")] public Udi ContentUdi { get; } /// @@ -46,7 +70,6 @@ namespace Umbraco.Cms.Core.Models.Blocks /// /// The content. /// - [DataMember(Name = "content")] public IPublishedElement Content { get; } /// @@ -55,8 +78,8 @@ namespace Umbraco.Cms.Core.Models.Blocks /// /// The settings UDI. /// - [DataMember(Name = "settingsUdi")] - public Udi SettingsUdi { get; } + [Obsolete("Use SettingsKey instead. Will be removed in V18.")] + public Udi? SettingsUdi { get; } /// /// Gets the settings. @@ -64,37 +87,31 @@ namespace Umbraco.Cms.Core.Models.Blocks /// /// The settings. /// - [DataMember(Name = "settings")] - public IPublishedElement Settings { get; } + public IPublishedElement? Settings { get; } /// /// The number of rows this item should span /// - [DataMember(Name = "rowSpan")] public int RowSpan { get; set; } /// /// The number of columns this item should span /// - [DataMember(Name = "columnSpan")] public int ColumnSpan { get; set; } /// /// The grid areas within this item /// - [DataMember(Name = "areas")] public IEnumerable Areas { get; set; } = Array.Empty(); /// /// The number of columns available for the areas to span /// - [DataMember(Name = "areaGridColumns")] public int? AreaGridColumns { get; set; } /// /// The number of columns in the root grid /// - [DataMember(Name = "gridColumns")] public int? GridColumns { get; set; } } @@ -112,12 +129,19 @@ namespace Umbraco.Cms.Core.Models.Blocks /// The content. /// The settings UDI. /// The settings. + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockGridItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) : base(contentUdi, content, settingsUdi, settings) { Content = content; } + public BlockGridItem(Guid contentKey, T content, Guid? settingsKey, IPublishedElement? settings) + : base(contentKey, content, settingsKey, settings) + { + Content = content; + } + /// /// Gets the content. /// @@ -143,18 +167,25 @@ namespace Umbraco.Cms.Core.Models.Blocks /// The content. /// The settings udi. /// The settings. + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockGridItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) : base(contentUdi, content, settingsUdi, settings) { Settings = settings; } + public BlockGridItem(Guid contentKey, TContent content, Guid? settingsKey, TSettings? settings) + : base(contentKey, content, settingsKey, settings) + { + Settings = settings; + } + /// /// Gets the settings. /// /// /// The settings. /// - public new TSettings Settings { get; } + public new TSettings? Settings { get; } } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockGridLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/BlockGridLayoutItem.cs index bd24a0d5f5..ff977acf98 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockGridLayoutItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockGridLayoutItem.cs @@ -6,12 +6,8 @@ namespace Umbraco.Cms.Core.Models.Blocks; /// /// Used for deserializing the block grid layout /// -public class BlockGridLayoutItem : IBlockLayoutItem +public class BlockGridLayoutItem : BlockLayoutItemBase { - public Udi? ContentUdi { get; set; } - - public Udi? SettingsUdi { get; set; } - public int? ColumnSpan { get; set; } public int? RowSpan { get; set; } @@ -21,10 +17,25 @@ public class BlockGridLayoutItem : IBlockLayoutItem public BlockGridLayoutItem() { } + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockGridLayoutItem(Udi contentUdi) - => ContentUdi = contentUdi; + : base(contentUdi) + { + } + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockGridLayoutItem(Udi contentUdi, Udi settingsUdi) - : this(contentUdi) - => SettingsUdi = settingsUdi; + : base(contentUdi, settingsUdi) + { + } + + public BlockGridLayoutItem(Guid contentKey) + : base(contentKey) + { + } + + public BlockGridLayoutItem(Guid contentKey, Guid settingsKey) + : base(contentKey, settingsKey) + { + } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockGridValue.cs b/src/Umbraco.Core/Models/Blocks/BlockGridValue.cs index 650f4e4754..e7f7c1ca17 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockGridValue.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockGridValue.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace Umbraco.Cms.Core.Models.Blocks; /// @@ -19,5 +21,6 @@ public class BlockGridValue : BlockValue => Layout[PropertyEditorAlias] = layouts; /// + [JsonIgnore] public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.BlockGrid; } diff --git a/src/Umbraco.Core/Models/Blocks/BlockItemData.cs b/src/Umbraco.Core/Models/Blocks/BlockItemData.cs index 75d34ba8c6..e99e8010e7 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockItemData.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockItemData.cs @@ -14,10 +14,20 @@ public class BlockItemData { } + [Obsolete("Use constructor that accepts GUID key instead. Will be removed in V18.")] public BlockItemData(Udi udi, Guid contentTypeKey, string contentTypeAlias) + : this( + (udi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(udi)), + contentTypeKey, + contentTypeAlias) + { + } + + public BlockItemData(Guid key, Guid contentTypeKey, string contentTypeAlias) { ContentTypeAlias = contentTypeAlias; - Udi = udi; + Key = key; + Udi = new GuidUdi(Constants.UdiEntityType.Element, key); ContentTypeKey = contentTypeKey; } @@ -29,43 +39,14 @@ public class BlockItemData [JsonIgnore] public string ContentTypeAlias { get; set; } = string.Empty; + [Obsolete("Use Key instead. Will be removed in V18.")] public Udi? Udi { get; set; } - [JsonIgnore] - public Guid Key => Udi is not null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned"); + public Guid Key { get; set; } - /// - /// The remaining properties will be serialized to a dictionary - /// - /// - /// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket - /// https://docs.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonextensiondataattribute - /// NestedContent serializes to string, int, whatever eg - /// "stringValue":"Some String","numericValue":125,"otherNumeric":null - /// + public IList Values { get; set; } = new List(); + + [Obsolete("Use Properties instead. Will be removed in V18.")] [JsonExtensionData] public Dictionary RawPropertyValues { get; set; } = new(); - - /// - /// Used during deserialization to convert the raw property data into data with a property type context - /// - [JsonIgnore] - public IDictionary PropertyValues { get; set; } = - new Dictionary(); - - /// - /// Used during deserialization to populate the property value/property type of a block item content property - /// - public class BlockPropertyValue - { - public BlockPropertyValue(object? value, IPropertyType propertyType) - { - Value = value; - PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); - } - - public object? Value { get; } - - public IPropertyType PropertyType { get; } - } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockItemVariation.cs b/src/Umbraco.Core/Models/Blocks/BlockItemVariation.cs new file mode 100644 index 0000000000..bb8dfcff22 --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockItemVariation.cs @@ -0,0 +1,21 @@ +namespace Umbraco.Cms.Core.Models.Blocks; + +public class BlockItemVariation +{ + public BlockItemVariation() + { + } + + public BlockItemVariation(Guid contentKey, string? culture, string? segment) + { + ContentKey = contentKey; + Culture = culture; + Segment = segment; + } + + public Guid ContentKey { get; set; } + + public string? Culture { get; set; } + + public string? Segment { get; set; } +} diff --git a/src/Umbraco.Core/Models/Blocks/BlockLayoutItemBase.cs b/src/Umbraco.Core/Models/Blocks/BlockLayoutItemBase.cs new file mode 100644 index 0000000000..4f4ff9e22a --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockLayoutItemBase.cs @@ -0,0 +1,84 @@ +namespace Umbraco.Cms.Core.Models.Blocks; + +public abstract class BlockLayoutItemBase : IBlockLayoutItem +{ + private Guid? _contentKey; + private Guid? _settingsKey; + + private Udi? _contentUdi; + private Udi? _settingsUdi; + + [Obsolete("Use ContentKey instead. Will be removed in V18.")] + public Udi? ContentUdi + { + get => _contentUdi; + set + { + if (_contentKey is not null) + { + return; + } + + _contentUdi = value; + _contentKey = (value as GuidUdi)?.Guid; + } + } + + [Obsolete("Use SettingsKey instead. Will be removed in V18.")] + public Udi? SettingsUdi + { + get => _settingsUdi; + set + { + if (_settingsKey is not null) + { + return; + } + + _settingsUdi = value; + _settingsKey = (value as GuidUdi)?.Guid; + } + } + + public Guid ContentKey + { + get => _contentKey ?? throw new InvalidOperationException("ContentKey has not yet been initialized"); + set => _contentKey = value; + } + + public Guid? SettingsKey + { + get => _settingsKey; + set => _settingsKey = value; + } + + protected BlockLayoutItemBase() + { } + + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] + protected BlockLayoutItemBase(Udi contentUdi) + : this((contentUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(contentUdi))) + { + } + + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] + protected BlockLayoutItemBase(Udi contentUdi, Udi settingsUdi) + : this( + (contentUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(contentUdi)), + (settingsUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(settingsUdi))) + { + } + + protected BlockLayoutItemBase(Guid contentKey) + { + ContentKey = contentKey; + ContentUdi = new GuidUdi(Constants.UdiEntityType.Element, contentKey); + } + + protected BlockLayoutItemBase(Guid contentKey, Guid settingsKey) + : this(contentKey) + { + SettingsKey = settingsKey; + SettingsUdi = new GuidUdi(Constants.UdiEntityType.Element, settingsKey); + } +} diff --git a/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs index 7842c66a28..e213be5f12 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Models.Blocks; @@ -12,17 +10,11 @@ namespace Umbraco.Cms.Core.Models.Blocks; /// public class BlockListEditorDataConverter : BlockEditorDataConverter { - [Obsolete("Use the constructor that takes IJsonSerializer. Will be removed in V15.")] - public BlockListEditorDataConverter() - : this(StaticServiceProvider.Instance.GetRequiredService()) - { - } - public BlockListEditorDataConverter(IJsonSerializer jsonSerializer) - : base(Constants.PropertyEditors.Aliases.BlockList, jsonSerializer) + : base(jsonSerializer) { } protected override IEnumerable GetBlockReferences(IEnumerable layout) - => layout.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); + => layout.Select(x => new ContentAndSettingsReference(x.ContentKey, x.SettingsKey)).ToList(); } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs index 6ccc4080e2..7e14a1b1e8 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs @@ -25,11 +25,25 @@ public class BlockListItem : IBlockReference + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) + : this( + (contentUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(contentUdi)), + content, + (settingsUdi as GuidUdi)?.Guid, + settings) { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + } + + public BlockListItem(Guid contentKey, IPublishedElement content, Guid? settingsKey, IPublishedElement? settings) + { + ContentKey = contentKey; + ContentUdi = new GuidUdi(Constants.UdiEntityType.Element, contentKey); Content = content ?? throw new ArgumentNullException(nameof(content)); - SettingsUdi = settingsUdi; + SettingsKey = settingsKey; + SettingsUdi = settingsKey.HasValue + ? new GuidUdi(Constants.UdiEntityType.Element, settingsKey.Value) + : null; Settings = settings; } @@ -39,7 +53,6 @@ public class BlockListItem : IBlockReference /// The content. /// - [DataMember(Name = "content")] public IPublishedElement Content { get; } /// @@ -48,8 +61,8 @@ public class BlockListItem : IBlockReference /// The settings UDI. /// - [DataMember(Name = "settingsUdi")] - public Udi SettingsUdi { get; } + [Obsolete("Use SettingsKey instead. Will be removed in V18.")] + public Udi? SettingsUdi { get; } /// /// Gets the content UDI. @@ -57,17 +70,26 @@ public class BlockListItem : IBlockReference /// The content UDI. /// - [DataMember(Name = "contentUdi")] + [Obsolete("Use ContentKey instead. Will be removed in V18.")] public Udi ContentUdi { get; } + /// + /// Gets the content key. + /// + public Guid ContentKey { get; set; } + + /// + /// Gets the settings key. + /// + public Guid? SettingsKey { get; set; } + /// /// Gets the settings. /// /// /// The settings. /// - [DataMember(Name = "settings")] - public IPublishedElement Settings { get; } + public IPublishedElement? Settings { get; } } /// @@ -85,10 +107,15 @@ public class BlockListItem : BlockListItem /// The content. /// The settings UDI. /// The settings. + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockListItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) : base(contentUdi, content, settingsUdi, settings) => Content = content; + public BlockListItem(Guid contentKey, T content, Guid? settingsKey, IPublishedElement? settings) + : base(contentKey, content, settingsKey, settings) => + Content = content; + /// /// Gets the content. /// @@ -115,15 +142,20 @@ public class BlockListItem : BlockListItem /// The content. /// The settings udi. /// The settings. + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockListItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) : base(contentUdi, content, settingsUdi, settings) => Settings = settings; + public BlockListItem(Guid contentKey, TContent content, Guid? settingsKey, TSettings? settings) + : base(contentKey, content, settingsKey, settings) => + Settings = settings; + /// /// Gets the settings. /// /// /// The settings. /// - public new TSettings Settings { get; } + public new TSettings? Settings { get; } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs index 4412257add..cffa234156 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs @@ -6,19 +6,30 @@ namespace Umbraco.Cms.Core.Models.Blocks; /// /// Used for deserializing the block list layout /// -public class BlockListLayoutItem : IBlockLayoutItem +public class BlockListLayoutItem : BlockLayoutItemBase { - public Udi? ContentUdi { get; set; } - - public Udi? SettingsUdi { get; set; } - public BlockListLayoutItem() { } + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockListLayoutItem(Udi contentUdi) - => ContentUdi = contentUdi; + : base(contentUdi) + { + } + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockListLayoutItem(Udi contentUdi, Udi settingsUdi) - : this(contentUdi) - => SettingsUdi = settingsUdi; + : base(contentUdi, settingsUdi) + { + } + + public BlockListLayoutItem(Guid contentKey) + : base(contentKey) + { + } + + public BlockListLayoutItem(Guid contentKey, Guid settingsKey) + : base(contentKey, settingsKey) + { + } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListValue.cs b/src/Umbraco.Core/Models/Blocks/BlockListValue.cs index d06277f71f..fe6524f88d 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListValue.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListValue.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace Umbraco.Cms.Core.Models.Blocks; /// @@ -19,5 +21,6 @@ public class BlockListValue : BlockValue => Layout[PropertyEditorAlias] = layouts; /// + [JsonIgnore] public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.BlockList; } diff --git a/src/Umbraco.Core/Models/Blocks/BlockPropertyValue.cs b/src/Umbraco.Core/Models/Blocks/BlockPropertyValue.cs new file mode 100644 index 0000000000..0b27f16681 --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockPropertyValue.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Core.Models.Blocks; + +public sealed class BlockPropertyValue : ValueModelBase +{ + // Used during deserialization to populate the property value/property type of a block item content property + [JsonIgnore] + public IPropertyType? PropertyType { get; set; } + + public string? EditorAlias => PropertyType?.PropertyEditorAlias; +} diff --git a/src/Umbraco.Core/Models/Blocks/BlockValue.cs b/src/Umbraco.Core/Models/Blocks/BlockValue.cs index bfe84430f8..1c25c8d434 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockValue.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockValue.cs @@ -32,6 +32,14 @@ public abstract class BlockValue /// public List SettingsData { get; set; } = []; + /// + /// Gets or sets the availability of blocks per variation. + /// + /// + /// Only applicable for block level variance. + /// + public IList Expose { get; set; } = new List(); + /// /// Gets the property editor alias of the current layout. /// @@ -39,6 +47,9 @@ public abstract class BlockValue /// The property editor alias of the current layout. /// public abstract string PropertyEditorAlias { get; } + + [Obsolete("Will be removed in V18.")] + public virtual bool SupportsBlockLayoutAlias(string alias) => alias.Equals(PropertyEditorAlias); } /// diff --git a/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs b/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs index 61b95235cd..f26db3b46d 100644 --- a/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs +++ b/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs @@ -5,16 +5,34 @@ namespace Umbraco.Cms.Core.Models.Blocks; public struct ContentAndSettingsReference : IEquatable { + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public ContentAndSettingsReference(Udi? contentUdi, Udi? settingsUdi) + : this( + (contentUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(contentUdi)), + (settingsUdi as GuidUdi)?.Guid) { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); - SettingsUdi = settingsUdi; } + public ContentAndSettingsReference(Guid contentKey, Guid? settingsKey) + { + ContentKey = contentKey; + SettingsKey = settingsKey; + ContentUdi = new GuidUdi(Constants.UdiEntityType.Element, contentKey); + SettingsUdi = settingsKey.HasValue + ? new GuidUdi(Constants.UdiEntityType.Element, settingsKey.Value) + : null; + } + + [Obsolete("Use ContentKey instead. Will be removed in V18.")] public Udi ContentUdi { get; } + [Obsolete("Use SettingsKey instead. Will be removed in V18.")] public Udi? SettingsUdi { get; } + public Guid ContentKey { get; set; } + + public Guid? SettingsKey { get; set; } + public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right) => left.Equals(right); diff --git a/src/Umbraco.Core/Models/Blocks/IBlockLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/IBlockLayoutItem.cs index eb5d3b0553..3974bfc1a0 100644 --- a/src/Umbraco.Core/Models/Blocks/IBlockLayoutItem.cs +++ b/src/Umbraco.Core/Models/Blocks/IBlockLayoutItem.cs @@ -5,7 +5,13 @@ namespace Umbraco.Cms.Core.Models.Blocks; public interface IBlockLayoutItem { + [Obsolete("Use ContentKey instead. Will be removed in V18.")] public Udi? ContentUdi { get; set; } + [Obsolete("Use SettingsKey instead. Will be removed in V18.")] public Udi? SettingsUdi { get; set; } + + public Guid ContentKey { get; set; } + + public Guid? SettingsKey { get; set; } } diff --git a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs index 647d1f5b2f..2505183efa 100644 --- a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs +++ b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs @@ -37,7 +37,7 @@ public interface IBlockReference : IBlockReference /// /// The settings. /// - TSettings Settings { get; } + TSettings? Settings { get; } } diff --git a/src/Umbraco.Core/Models/Blocks/RichTextBlockItem.cs b/src/Umbraco.Core/Models/Blocks/RichTextBlockItem.cs index f5be6f9e23..17abbe1d90 100644 --- a/src/Umbraco.Core/Models/Blocks/RichTextBlockItem.cs +++ b/src/Umbraco.Core/Models/Blocks/RichTextBlockItem.cs @@ -25,21 +25,38 @@ public class RichTextBlockItem : IBlockReference + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public RichTextBlockItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) + : this( + (contentUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(contentUdi)), + content, + (settingsUdi as GuidUdi)?.Guid, + settings) { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + } + + public RichTextBlockItem(Guid contentKey, IPublishedElement content, Guid? settingsKey, IPublishedElement? settings) + { + ContentKey = contentKey; + ContentUdi = new GuidUdi(Constants.UdiEntityType.Element, contentKey); Content = content ?? throw new ArgumentNullException(nameof(content)); - SettingsUdi = settingsUdi; + SettingsKey = settingsKey; + SettingsUdi = settingsKey.HasValue + ? new GuidUdi(Constants.UdiEntityType.Element, settingsKey.Value) + : null; Settings = settings; } + public Guid ContentKey { get; set; } + + public Guid? SettingsKey { get; set; } + /// /// Gets the content. /// /// /// The content. /// - [DataMember(Name = "content")] public IPublishedElement Content { get; } /// @@ -48,8 +65,8 @@ public class RichTextBlockItem : IBlockReference /// The settings UDI. /// - [DataMember(Name = "settingsUdi")] - public Udi SettingsUdi { get; } + [Obsolete("Use SettingsKey instead. Will be removed in V18.")] + public Udi? SettingsUdi { get; } /// /// Gets the content UDI. @@ -57,7 +74,7 @@ public class RichTextBlockItem : IBlockReference /// The content UDI. /// - [DataMember(Name = "contentUdi")] + [Obsolete("Use ContentKey instead. Will be removed in V18.")] public Udi ContentUdi { get; } /// @@ -66,8 +83,7 @@ public class RichTextBlockItem : IBlockReference /// The settings. /// - [DataMember(Name = "settings")] - public IPublishedElement Settings { get; } + public IPublishedElement? Settings { get; } } /// @@ -85,10 +101,15 @@ public class RichTextBlockItem : RichTextBlockItem /// The content. /// The settings UDI. /// The settings. + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public RichTextBlockItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) : base(contentUdi, content, settingsUdi, settings) => Content = content; + public RichTextBlockItem(Guid contentKey, T content, Guid? settingsKey, IPublishedElement? settings) + : base(contentKey, content, settingsKey, settings) => + Content = content; + /// /// Gets the content. /// @@ -115,15 +136,20 @@ public class RichTextBlockItem : RichTextBlockItemThe content. /// The settings udi. /// The settings. + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public RichTextBlockItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) : base(contentUdi, content, settingsUdi, settings) => Settings = settings; + public RichTextBlockItem(Guid contentKey, TContent content, Guid? settingsKey, TSettings? settings) + : base(contentKey, content, settingsKey, settings) => + Settings = settings; + /// /// Gets the settings. /// /// /// The settings. /// - public new TSettings Settings { get; } + public new TSettings? Settings { get; } } diff --git a/src/Umbraco.Core/Models/Blocks/RichTextBlockLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/RichTextBlockLayoutItem.cs index 0cd7210443..57d69003a1 100644 --- a/src/Umbraco.Core/Models/Blocks/RichTextBlockLayoutItem.cs +++ b/src/Umbraco.Core/Models/Blocks/RichTextBlockLayoutItem.cs @@ -6,19 +6,30 @@ namespace Umbraco.Cms.Core.Models.Blocks; /// /// Used for deserializing the rich text block layouts /// -public class RichTextBlockLayoutItem : IBlockLayoutItem +public class RichTextBlockLayoutItem : BlockLayoutItemBase { - public Udi? ContentUdi { get; set; } - - public Udi? SettingsUdi { get; set; } - public RichTextBlockLayoutItem() { } + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public RichTextBlockLayoutItem(Udi contentUdi) - => ContentUdi = contentUdi; + : base(contentUdi) + { + } + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public RichTextBlockLayoutItem(Udi contentUdi, Udi settingsUdi) - : this(contentUdi) - => SettingsUdi = settingsUdi; + : base(contentUdi, settingsUdi) + { + } + + public RichTextBlockLayoutItem(Guid contentKey) + : base(contentKey) + { + } + + public RichTextBlockLayoutItem(Guid contentKey, Guid settingsKey) + : base(contentKey, settingsKey) + { + } } diff --git a/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs b/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs index 728c06152e..efae15d0a1 100644 --- a/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs +++ b/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace Umbraco.Cms.Core.Models.Blocks; /// @@ -19,5 +21,11 @@ public class RichTextBlockValue : BlockValue => Layout[PropertyEditorAlias] = layouts; /// - public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.TinyMce; + [JsonIgnore] + public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.RichText; + + // RTE block layouts uses "Umbraco.TinyMCE" in V14 and below, but should use "Umbraco.RichText" for V15+ + [Obsolete("Will be removed in V18.")] + public override bool SupportsBlockLayoutAlias(string alias) + => base.SupportsBlockLayoutAlias(alias) || alias.Equals(Constants.PropertyEditors.Aliases.TinyMce); } diff --git a/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs b/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs index 183dabe420..508b55202c 100644 --- a/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs @@ -7,17 +7,11 @@ namespace Umbraco.Cms.Core.Models.Blocks; /// public sealed class RichTextEditorBlockDataConverter : BlockEditorDataConverter { - [Obsolete("Use the constructor that takes IJsonSerializer. Will be removed in V15.")] - public RichTextEditorBlockDataConverter() - : base(Constants.PropertyEditors.Aliases.TinyMce) - { - } - public RichTextEditorBlockDataConverter(IJsonSerializer jsonSerializer) : base(jsonSerializer) { } protected override IEnumerable GetBlockReferences(IEnumerable layout) - => layout.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); + => layout.Select(x => new ContentAndSettingsReference(x.ContentKey, x.SettingsKey)).ToList(); } diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index 5d67a4a974..22bbfae7c7 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -1,4 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; namespace Umbraco.Extensions; @@ -287,21 +290,22 @@ public static class ContentRepositoryExtensions } } + [Obsolete("Please use the overload that accepts all parameters. Will be removed in V16.")] + public static bool PublishCulture(this IContent content, CultureImpact? impact) + => PublishCulture(content, impact, DateTime.Now, StaticServiceProvider.Instance.GetRequiredService()); + /// /// Sets the publishing values for names and properties. /// /// /// + /// + /// /// /// A value indicating whether it was possible to publish the names and values for the specified /// culture(s). The method may fail if required names are not set, but it does NOT validate property data /// - /// - public static bool PublishCulture(this IContent content, CultureImpact? impact) - { - return PublishCulture(content, impact, DateTime.Now); - } - public static bool PublishCulture(this IContent content, CultureImpact? impact, DateTime publishTime) + public static bool PublishCulture(this IContent content, CultureImpact? impact, DateTime publishTime, PropertyEditorCollection propertyEditorCollection) { if (impact == null) { @@ -356,13 +360,13 @@ public static class ContentRepositoryExtensions foreach (IProperty property in content.Properties) { // for the specified culture (null or all or specific) - property.PublishValues(impact.Culture); + PublishPropertyValues(content, property, impact.Culture, propertyEditorCollection); // maybe the specified culture did not impact the invariant culture, so PublishValues // above would skip it, yet it *also* impacts invariant properties if (impact.ImpactsAlsoInvariantProperties && (property.PropertyType.VariesByCulture() is false || impact.ImpactsOnlyDefaultCulture)) { - property.PublishValues(null); + PublishPropertyValues(content, property, null, propertyEditorCollection); } } @@ -370,6 +374,22 @@ public static class ContentRepositoryExtensions return true; } + private static void PublishPropertyValues(IContent content, IProperty property, string? culture, PropertyEditorCollection propertyEditorCollection) + { + // if the content varies by culture, let data editor opt-in to perform partial property publishing (per culture) + if (content.ContentType.VariesByCulture() + && propertyEditorCollection.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) + && dataEditor.CanMergePartialPropertyValues(property.PropertyType)) + { + // perform partial publishing for the current culture + property.PublishPartialValues(dataEditor, culture); + return; + } + + // for the specified culture (null or all or specific) + property.PublishValues(culture); + } + /// /// Returns false if the culture is already unpublished /// diff --git a/src/Umbraco.Core/Models/IProperty.cs b/src/Umbraco.Core/Models/IProperty.cs index 54f1e8581f..d9a57e2558 100644 --- a/src/Umbraco.Core/Models/IProperty.cs +++ b/src/Umbraco.Core/Models/IProperty.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.PropertyEditors; namespace Umbraco.Cms.Core.Models; @@ -35,5 +36,7 @@ public interface IProperty : IEntity, IRememberBeingDirty void PublishValues(string? culture = "*", string segment = "*"); + void PublishPartialValues(IDataEditor dataEditor, string? culture); + void UnpublishValues(string? culture = "*", string segment = "*"); } diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index a4e8eb056c..b756b143ad 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -2,6 +2,7 @@ using System.Runtime.Serialization; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models; @@ -174,6 +175,21 @@ public class Property : EntityBase, IProperty : null; } + // internal - must be invoked by the content item + // does *not* validate the value - content item must validate first + public void PublishPartialValues(IDataEditor dataEditor, string? culture) + { + if (PropertyType.VariesByCulture()) + { + throw new NotSupportedException("Cannot publish merged culture values for culture variant properties"); + } + + culture = culture?.NullOrWhiteSpaceAsNull(); + + var value = dataEditor.MergePartialPropertyValueForCulture(_pvalue?.EditedValue, _pvalue?.PublishedValue, culture); + PublishValue(_pvalue, value); + } + // internal - must be invoked by the content item // does *not* validate the value - content item must validate first public void PublishValues(string? culture = "*", string? segment = "*") @@ -300,13 +316,23 @@ public class Property : EntityBase, IProperty return; } + PublishValue(pvalue, ConvertAssignedValue(pvalue.EditedValue)); + } + + private void PublishValue(IPropertyValue? pvalue, object? newPublishedValue) + { + if (pvalue == null) + { + return; + } + if (!PropertyType.SupportsPublishing) { throw new NotSupportedException("Property type does not support publishing."); } var origValue = pvalue.PublishedValue; - pvalue.PublishedValue = ConvertAssignedValue(pvalue.EditedValue); + pvalue.PublishedValue = newPublishedValue; DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); } diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index 3048162891..cb7a43ba78 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -196,4 +196,10 @@ public class DataEditor : IDataEditor /// Provides a summary of the PropertyEditor for use with the . /// protected virtual string DebuggerDisplay() => $"Alias: {Alias}"; + + /// + public virtual bool CanMergePartialPropertyValues(IPropertyType propertyType) => false; + + /// + public virtual object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) => sourceValue; } diff --git a/src/Umbraco.Core/PropertyEditors/IDataEditor.cs b/src/Umbraco.Core/PropertyEditors/IDataEditor.cs index 5a95445fce..a9d3b896e7 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataEditor.cs @@ -51,4 +51,18 @@ public interface IDataEditor : IDiscoverable /// Is expected to throw if the editor does not support being configured, e.g. for most parameter editors. /// IConfigurationEditor GetConfigurationEditor(); + + /// + /// Determines if the value editor needs to perform for a given property type. + /// + bool CanMergePartialPropertyValues(IPropertyType propertyType) => false; + + /// + /// Partially merges a source property value into a target property value for a given culture. + /// + /// The source property value. + /// The target property value. + /// The culture (or null for invariant). + /// The result of the merge operation. + object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) => sourceValue; } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index c667ea0b35..d73528984a 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Services.Navigation; @@ -35,6 +36,7 @@ public class ContentService : RepositoryService, IContentService private readonly ICultureImpactFactory _cultureImpactFactory; private readonly IUserIdKeyResolver _userIdKeyResolver; private readonly IDocumentNavigationManagementService _documentNavigationManagementService; + private readonly PropertyEditorCollection _propertyEditorCollection; private IQuery? _queryNotTrashed; #region Constructors @@ -53,7 +55,8 @@ public class ContentService : RepositoryService, IContentService IShortStringHelper shortStringHelper, ICultureImpactFactory cultureImpactFactory, IUserIdKeyResolver userIdKeyResolver, - IDocumentNavigationManagementService documentNavigationManagementService) + IDocumentNavigationManagementService documentNavigationManagementService, + PropertyEditorCollection propertyEditorCollection) : base(provider, loggerFactory, eventMessagesFactory) { _documentRepository = documentRepository; @@ -67,6 +70,7 @@ public class ContentService : RepositoryService, IContentService _cultureImpactFactory = cultureImpactFactory; _userIdKeyResolver = userIdKeyResolver; _documentNavigationManagementService = documentNavigationManagementService; + _propertyEditorCollection = propertyEditorCollection; _logger = loggerFactory.CreateLogger(); } @@ -99,11 +103,12 @@ public class ContentService : RepositoryService, IContentService shortStringHelper, cultureImpactFactory, userIdKeyResolver, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } - [Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")] + [Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")] public ContentService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, @@ -131,7 +136,8 @@ public class ContentService : RepositoryService, IContentService shortStringHelper, cultureImpactFactory, StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -1244,7 +1250,7 @@ public class ContentService : RepositoryService, IContentService var publishTime = DateTime.Now; foreach (CultureImpact? impact in impacts) { - content.PublishCulture(impact, publishTime); + content.PublishCulture(impact, publishTime, _propertyEditorCollection); } // Change state to publishing @@ -1881,7 +1887,7 @@ public class ContentService : RepositoryService, IContentService // publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed IProperty[]? invalidProperties = null; CultureImpact impact = _cultureImpactFactory.ImpactExplicit(culture, IsDefaultCulture(allLangs.Value, culture)); - var tryPublish = d.PublishCulture(impact, date) && + var tryPublish = d.PublishCulture(impact, date, _propertyEditorCollection) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact); if (invalidProperties != null && invalidProperties.Length > 0) { @@ -1965,12 +1971,12 @@ public class ContentService : RepositoryService, IContentService return culturesToPublish.All(culture => { CultureImpact? impact = _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content); - return content.PublishCulture(impact, publishTime) && + return content.PublishCulture(impact, publishTime, _propertyEditorCollection) && _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact); }); } - return content.PublishCulture(_cultureImpactFactory.ImpactInvariant(), publishTime) + return content.PublishCulture(_cultureImpactFactory.ImpactInvariant(), publishTime, _propertyEditorCollection) && _propertyValidationService.Value.IsPropertyDataValid(content, out _, _cultureImpactFactory.ImpactInvariant()); } @@ -3197,7 +3203,7 @@ public class ContentService : RepositoryService, IContentService // publish the culture(s) var publishTime = DateTime.Now; - if (!impactsToPublish.All(impact => content.PublishCulture(impact, publishTime))) + if (!impactsToPublish.All(impact => content.PublishCulture(impact, publishTime, _propertyEditorCollection))) { return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content); } diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs index 3860d3c32e..ec2d710087 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs @@ -166,18 +166,18 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich private void CleanUpBlocks(string tag, Dictionary attributes) { - if (tag.StartsWith("umb-rte-block") is false || attributes.ContainsKey("data-content-udi") is false || attributes["data-content-udi"] is not string dataUdi) + if (tag.StartsWith("umb-rte-block") is false || attributes.ContainsKey(BlockContentKeyAttribute) is false || attributes[BlockContentKeyAttribute] is not string dataKey) { return; } - if (UdiParser.TryParse(dataUdi, out GuidUdi? guidUdi) is false) + if (Guid.TryParse(dataKey, out Guid key) is false) { return; } - attributes["content-id"] = guidUdi.Guid; - attributes.Remove("data-content-udi"); + attributes["content-id"] = key; + attributes.Remove(BlockContentKeyAttribute); } private static void SanitizeAttributes(Dictionary attributes) diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs index c6a4a3c956..fcb55258d0 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs @@ -109,15 +109,15 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT HtmlNode[] blocks = doc.DocumentNode.SelectNodes("//*[starts-with(local-name(),'umb-rte-block')]")?.ToArray() ?? Array.Empty(); foreach (HtmlNode block in blocks) { - var dataUdi = block.GetAttributeValue("data-content-udi", string.Empty); - if (UdiParser.TryParse(dataUdi, out GuidUdi? guidUdi) is false) + var dataKey = block.GetAttributeValue(BlockContentKeyAttribute, string.Empty); + if (Guid.TryParse(dataKey, out Guid key) is false) { continue; } // swap the content UDI for the content ID - block.Attributes.Remove("data-content-udi"); - block.SetAttributeValue("data-content-id", guidUdi.Guid.ToString("D")); + block.Attributes.Remove(BlockContentKeyAttribute); + block.SetAttributeValue("data-content-id", key.ToString("D")); // remove the inner comment placed by the RTE block.RemoveAllChildren(); diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs index 407bc1a022..dd88453fab 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs @@ -13,6 +13,8 @@ internal abstract partial class ApiRichTextParserBase private readonly IApiContentRouteBuilder _apiContentRouteBuilder; private readonly IApiMediaUrlProvider _apiMediaUrlProvider; + protected const string BlockContentKeyAttribute = "data-content-key"; + protected ApiRichTextParserBase(IApiContentRouteBuilder apiContentRouteBuilder, IApiMediaUrlProvider apiMediaUrlProvider) { _apiContentRouteBuilder = apiContentRouteBuilder; diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 6d99898cd6..cdb00380b9 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -162,6 +162,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // both SimpleTinyMceValueConverter (in Core) and RteBlockRenderingValueConverter (in Infrastructure) will be // discovered when CoreBootManager configures the converters. We will remove the basic one defined diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 649190f5f0..459af0304e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -99,5 +99,8 @@ public class UmbracoPlan : MigrationPlan // To 15.0.0 To("{7F4F31D8-DD71-4F0D-93FC-2690A924D84B}"); To("{1A8835EF-F8AB-4472-B4D8-D75B7C164022}"); + To("{6C04B137-0097-4938-8C6A-276DF1A0ECA8}"); + To("{9D3CE7D4-4884-41D4-98E8-302EB6CB0CF6}"); + To("{37875E80-5CDD-42FF-A21A-7D4E3E23E0ED}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesBase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesBase.cs new file mode 100644 index 0000000000..f60c7f049f --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesBase.cs @@ -0,0 +1,281 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; + +[Obsolete("Will be removed in V18")] +public abstract class ConvertBlockEditorPropertiesBase : MigrationBase +{ + private readonly ILogger _logger; + private readonly IContentTypeService _contentTypeService; + private readonly IDataTypeService _dataTypeService; + private readonly IJsonSerializer _jsonSerializer; + private readonly IUmbracoContextFactory _umbracoContextFactory; + private readonly ILanguageService _languageService; + + protected abstract IEnumerable PropertyEditorAliases { get; } + + protected abstract EditorValueHandling DetermineEditorValueHandling(object editorValue); + + protected bool SkipMigration { get; init; } + + protected enum EditorValueHandling + { + IgnoreConversion, + ProceedConversion, + HandleAsError + } + + public ConvertBlockEditorPropertiesBase( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService) + : base(context) + { + _logger = logger; + _contentTypeService = contentTypeService; + _dataTypeService = dataTypeService; + _jsonSerializer = jsonSerializer; + _umbracoContextFactory = umbracoContextFactory; + _languageService = languageService; + } + + protected override void Migrate() + { + if (SkipMigration) + { + _logger.LogInformation("Migration was skipped due to configuration."); + return; + } + + using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); + var languagesById = _languageService.GetAllAsync().GetAwaiter().GetResult().ToDictionary(language => language.Id); + IContentType[] allContentTypes = _contentTypeService.GetAll().ToArray(); + var allPropertyTypesByEditor = allContentTypes + .SelectMany(ct => ct.PropertyTypes) + .GroupBy(pt => pt.PropertyEditorAlias) + .ToDictionary(group => group.Key, group => group.ToArray()); + + foreach (var propertyEditorAlias in PropertyEditorAliases) + { + if (allPropertyTypesByEditor.TryGetValue(propertyEditorAlias, out IPropertyType[]? propertyTypes) is false) + { + continue; + } + + _logger.LogInformation("Migration starting for all properties of type: {propertyEditorAlias}", propertyEditorAlias); + if (Handle(propertyTypes, languagesById)) + { + _logger.LogInformation("Migration succeeded for all properties of type: {propertyEditorAlias}", propertyEditorAlias); + } + else + { + _logger.LogError("Migration failed for one or more properties of type: {propertyEditorAlias}", propertyEditorAlias); + } + } + } + + protected virtual object UpdateEditorValue(object editorValue) => editorValue; + + protected virtual string UpdateDatabaseValue(string dbValue) => dbValue; + + private bool Handle(IPropertyType[] propertyTypes, IDictionary languagesById) + { + var success = true; + + foreach (IPropertyType propertyType in propertyTypes) + { + try + { + _logger.LogInformation("- starting property type: {propertyTypeName} (id: {propertyTypeId}, alias: {propertyTypeAlias})...", propertyType.Name, propertyType.Id, propertyType.Alias); + IDataType dataType = _dataTypeService.GetAsync(propertyType.DataTypeKey).GetAwaiter().GetResult() + ?? throw new InvalidOperationException("The data type could not be fetched."); + + if (IsCandidateForMigration(propertyType, dataType) is false) + { + _logger.LogInformation(" - skipped property type migration because it was not a applicable."); + continue; + } + + IDataValueEditor valueEditor = dataType.Editor?.GetValueEditor() + ?? throw new InvalidOperationException("The data type value editor could not be fetched."); + + Sql sql = Sql() + .Select() + .From() + .Where(dto => dto.PropertyTypeId == propertyType.Id); + List propertyDataDtos = Database.Fetch(sql); + if (propertyDataDtos.Any() is false) + { + continue; + } + + var updateBatch = propertyDataDtos.Select(propertyDataDto => + UpdateBatch.For(propertyDataDto, Database.StartSnapshot(propertyDataDto))).ToList(); + + var updatesToSkip = new ConcurrentBag>(); + + var progress = 0; + + ExecutionContext.SuppressFlow(); + Parallel.ForEach(updateBatch, update => + { + using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); + + progress++; + if (progress % 100 == 0) + { + _logger.LogInformation(" - finíshed {progress} of {total} properties", progress, updateBatch.Count); + } + + PropertyDataDto propertyDataDto = update.Poco; + + // NOTE: some old property data DTOs can have variance defined, even if the property type no longer varies + var culture = propertyType.VariesByCulture() + && propertyDataDto.LanguageId.HasValue + && languagesById.TryGetValue(propertyDataDto.LanguageId.Value, out ILanguage? language) + ? language.IsoCode + : null; + + if (culture is null && propertyType.VariesByCulture()) + { + // if we end up here, the property DTO is bound to a language that no longer exists. this is an error scenario, + // and we can't really handle it in any other way than logging; in all likelihood this is an old property version, + // and it won't cause any runtime issues + _logger.LogWarning( + " - property data with id: {propertyDataId} references a language that does not exist - language id: {languageId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})", + propertyDataDto.Id, + propertyDataDto.LanguageId, + propertyType.Name, + propertyType.Id, + propertyType.Alias); + return; + } + + var segment = propertyType.VariesBySegment() ? propertyDataDto.Segment : null; + var property = new Property(propertyType); + property.SetValue(propertyDataDto.Value, culture, segment); + var toEditorValue = valueEditor.ToEditor(property, culture, segment); + switch (toEditorValue) + { + case null: + _logger.LogWarning( + " - value editor yielded a null value for property data with id: {propertyDataId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})", + propertyDataDto.Id, + propertyType.Name, + propertyType.Id, + propertyType.Alias); + updatesToSkip.Add(update); + return; + + case string str when str.IsNullOrWhiteSpace(): + // indicates either an empty block editor or corrupt block editor data - we can't do anything about either here + updatesToSkip.Add(update); + return; + + default: + switch (DetermineEditorValueHandling(toEditorValue)) + { + case EditorValueHandling.IgnoreConversion: + // nothing to convert, continue + updatesToSkip.Add(update); + return; + case EditorValueHandling.ProceedConversion: + // continue the conversion + break; + case EditorValueHandling.HandleAsError: + _logger.LogError( + " - value editor did not yield a valid ToEditor value for property data with id: {propertyDataId} - the value type was {valueType} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})", + propertyDataDto.Id, + toEditorValue.GetType(), + propertyType.Name, + propertyType.Id, + propertyType.Alias); + updatesToSkip.Add(update); + return; + default: + throw new ArgumentOutOfRangeException(); + } + break; + } + + toEditorValue = UpdateEditorValue(toEditorValue); + + var editorValue = _jsonSerializer.Serialize(toEditorValue); + var dbValue = valueEditor.FromEditor(new ContentPropertyData(editorValue, null), null); + if (dbValue is not string stringValue || stringValue.DetectIsJson() is false) + { + _logger.LogError( + " - value editor did not yield a valid JSON string as FromEditor value property data with id: {propertyDataId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})", + propertyDataDto.Id, + propertyType.Name, + propertyType.Id, + propertyType.Alias); + updatesToSkip.Add(update); + return; + } + + stringValue = UpdateDatabaseValue(stringValue); + + propertyDataDto.TextValue = stringValue; + }); + ExecutionContext.RestoreFlow(); + + updateBatch.RemoveAll(updatesToSkip.Contains); + + if (updateBatch.Any() is false) + { + _logger.LogInformation(" - no properties to convert, continuing"); + continue; + } + + _logger.LogInformation(" - {totalConverted} properties converted, saving...", updateBatch.Count); + var result = Database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 }); + if (result != updateBatch.Count) + { + throw new InvalidOperationException($"The database batch update was supposed to update {updateBatch.Count} property DTO entries, but it updated {result} entries."); + } + + _logger.LogDebug( + "Migration completed for property type: {propertyTypeName} (id: {propertyTypeId}, alias: {propertyTypeAlias}, editor alias: {propertyTypeEditorAlias}) - {updateCount} property DTO entries updated.", + propertyType.Name, + propertyType.Id, + propertyType.Alias, + propertyType.PropertyEditorAlias, + result); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Migration failed for property type: {propertyTypeName} (id: {propertyTypeId}, alias: {propertyTypeAlias}, editor alias: {propertyTypeEditorAlias})", + propertyType.Name, + propertyType.Id, + propertyType.Alias, + propertyType.PropertyEditorAlias); + + success = false; + } + } + + return success; + } + + protected virtual bool IsCandidateForMigration(IPropertyType propertyType, IDataType dataType) + => true; +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesOptions.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesOptions.cs new file mode 100644 index 0000000000..348b8e8e21 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesOptions.cs @@ -0,0 +1,29 @@ +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; + +[Obsolete("Will be removed in V18")] +public class ConvertBlockEditorPropertiesOptions +{ + /// + /// Setting this property to true will cause the migration of Block List editors to be skipped. + /// + /// + /// If you choose to skip the migration, you're responsible for performing the content migration for Block Lists after the V15 upgrade has completed. + /// + public bool SkipBlockListEditors { get; set; } = false; + + /// + /// Setting this property to true will cause the migration of Block Grid editors to be skipped. + /// + /// + /// If you choose to skip the migration, you're responsible for performing the content migration for Block Grids after the V15 upgrade has completed. + /// + public bool SkipBlockGridEditors { get; set; } = false; + + /// + /// Setting this property to true will cause the migration of Rich Text editors to be skipped. + /// + /// + /// If you choose to skip the migration, you're responsible for performing the content migration for Rich Texts after the V15 upgrade has completed. + /// + public bool SkipRichTextEditors { get; set; } = false; +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockGridEditorProperties.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockGridEditorProperties.cs new file mode 100644 index 0000000000..cc80a77ea9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockGridEditorProperties.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; + +[Obsolete("Will be removed in V18")] +public class ConvertBlockGridEditorProperties : ConvertBlockEditorPropertiesBase +{ + public ConvertBlockGridEditorProperties( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService, + IOptions options) + : base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService) + => SkipMigration = options.Value.SkipBlockGridEditors; + + protected override IEnumerable PropertyEditorAliases + => new[] { Constants.PropertyEditors.Aliases.BlockGrid }; + + protected override EditorValueHandling DetermineEditorValueHandling(object editorValue) + => editorValue is BlockValue blockValue + ? blockValue.ContentData.Any() + ? EditorValueHandling.ProceedConversion + : EditorValueHandling.IgnoreConversion + : EditorValueHandling.HandleAsError; + + public ConvertBlockGridEditorProperties( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService) + : base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService) + { + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockListEditorProperties.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockListEditorProperties.cs new file mode 100644 index 0000000000..e920a3b6d8 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockListEditorProperties.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; + +[Obsolete("Will be removed in V18")] +public class ConvertBlockListEditorProperties : ConvertBlockEditorPropertiesBase +{ + public ConvertBlockListEditorProperties( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService, + IOptions options) + : base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService) + => SkipMigration = options.Value.SkipBlockListEditors; + + protected override IEnumerable PropertyEditorAliases + => new[] { Constants.PropertyEditors.Aliases.BlockList }; + + protected override EditorValueHandling DetermineEditorValueHandling(object editorValue) + => editorValue is BlockValue blockValue + ? blockValue.ContentData.Any() + ? EditorValueHandling.ProceedConversion + : EditorValueHandling.IgnoreConversion + : EditorValueHandling.HandleAsError; + + public ConvertBlockListEditorProperties( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService) + : base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService) + { + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertRichTextEditorProperties.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertRichTextEditorProperties.cs new file mode 100644 index 0000000000..6023917b4c --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertRichTextEditorProperties.cs @@ -0,0 +1,74 @@ +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; + +[Obsolete("Will be removed in V18")] +public partial class ConvertRichTextEditorProperties : ConvertBlockEditorPropertiesBase +{ + public ConvertRichTextEditorProperties( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService, + IOptions options) + : base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService) + => SkipMigration = options.Value.SkipRichTextEditors; + + protected override IEnumerable PropertyEditorAliases + => new[] { Constants.PropertyEditors.Aliases.TinyMce, Constants.PropertyEditors.Aliases.RichText }; + + protected override EditorValueHandling DetermineEditorValueHandling(object editorValue) + => editorValue is RichTextEditorValue richTextEditorValue + ? richTextEditorValue.Blocks?.ContentData.Any() is true + ? EditorValueHandling.ProceedConversion + : EditorValueHandling.IgnoreConversion + : EditorValueHandling.HandleAsError; + + protected override object UpdateEditorValue(object editorValue) + { + if (editorValue is not RichTextEditorValue richTextEditorValue) + { + return base.UpdateEditorValue(editorValue); + } + + richTextEditorValue.Markup = BlockRegex().Replace( + richTextEditorValue.Markup, + match => UdiParser.TryParse(match.Groups["udi"].Value, out GuidUdi? guidUdi) + ? match.Value + .Replace(match.Groups["attribute"].Value, "data-content-key") + .Replace(match.Groups["udi"].Value, guidUdi.Guid.ToString("D")) + : string.Empty); + + return richTextEditorValue; + } + + public ConvertRichTextEditorProperties( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService) + : base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService) + { + } + + protected override bool IsCandidateForMigration(IPropertyType propertyType, IDataType dataType) + => dataType.ConfigurationObject is RichTextConfiguration richTextConfiguration + && richTextConfiguration.Blocks?.Any() is true; + + [GeneratedRegex("data-content-udi)=\"(?.[^\"]*)\".*<\\/umb-rte-block")] + private static partial Regex BlockRegex(); +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyNotificationHandlerBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyNotificationHandlerBase.cs index a2f5a3564e..bb1bf3bc64 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyNotificationHandlerBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyNotificationHandlerBase.cs @@ -11,26 +11,26 @@ public abstract class BlockEditorPropertyNotificationHandlerBase> _logger; - private readonly List _udisToReplace = new List(); + private readonly List _keysToReplace = new List(); protected BlockEditorPropertyNotificationHandlerBase(ILogger> logger) => _logger = logger; protected override string FormatPropertyValue(string rawJson, bool onlyMissingKeys) { - // the block editor doesn't ever have missing UDIs so when this is true there's nothing to process + // the block editor doesn't ever have missing keys so when this is true there's nothing to process if (onlyMissingKeys) { return rawJson; } - return ReplaceBlockEditorUdis(rawJson); + return ReplaceBlockEditorKeys(rawJson); } // internal for tests - // the purpose of this method is to replace the content and settings UDIs throughout the JSON structure of a block editor value. - // the challenge is nested block editor values, which must also have their UDIs replaced. this becomes particularly tricky because - // other nested property values could also contain UDIs, which should *not* be replaced (i.e. a content picker value). - internal string ReplaceBlockEditorUdis(string rawJson, Func? createGuid = null) + // the purpose of this method is to replace the content and settings keys throughout the JSON structure of a block editor value. + // the challenge is nested block editor values, which must also have their keys replaced. this becomes particularly tricky because + // other nested property values could also contain keys, which should *not* be replaced (i.e. a content picker value). + internal string ReplaceBlockEditorKeys(string rawJson, Func? createGuid = null) { // used so we can test nicely createGuid ??= _ => Guid.NewGuid(); @@ -52,21 +52,21 @@ public abstract class BlockEditorPropertyNotificationHandlerBase { - if (_udisToReplace.Contains(match.Value) == false) + if (_keysToReplace.Contains(match.Value) is false) { return match.Value; } - var oldKey = Guid.Parse(match.Groups[2].Value); + var oldKey = Guid.Parse(match.Value); if (oldToNewKeys.ContainsKey(oldKey) == false) { oldToNewKeys[oldKey] = createGuid(oldKey); } - return $"{match.Groups[1]}{oldToNewKeys[oldKey].ToString("N")}"; + return match.Value.Replace(match.Value, oldToNewKeys[oldKey].ToString("D")); }); return rawJson; @@ -109,7 +109,7 @@ public abstract class BlockEditorPropertyNotificationHandlerBase c?["udi"]) - .Union(settingsData.Select(s => s?["udi"])) - .Select(udiToken => udiToken?.GetValue().NullOrWhiteSpaceAsNull()) + // grab all keys from the objects of contentData and settingsData + var keys = contentData.Select(c => c?["key"]) + .Union(settingsData.Select(s => s?["key"])) + .Select(keyToken => keyToken?.GetValue().NullOrWhiteSpaceAsNull()) .ToArray(); // the following is solely for avoiding functionality wise breakage. we should consider removing it eventually, but for the time being it's harmless. - foreach (var udiToReplace in udis) + foreach (var keyToReplace in keys) { - if (UdiParser.TryParse(udiToReplace ?? string.Empty, out Udi? udi) == false || udi is not GuidUdi) + if (Guid.TryParse(keyToReplace ?? string.Empty, out Guid _) is false) { - throw new FormatException($"Could not parse a valid {nameof(GuidUdi)} from the string: \"{udiToReplace}\""); + throw new FormatException($"Could not parse a valid {nameof(Guid)} from the string: \"{keyToReplace}\""); } } - _udisToReplace.AddRange(udis.WhereNotNull()); + _keysToReplace.AddRange(keys.WhereNotNull()); foreach (JsonObject item in contentData.Union(settingsData).WhereNotNull().OfType()) { - foreach (JsonNode property in item.Where(p => p.Key != "contentTypeKey" && p.Key != "udi").Select(p => p.Value).WhereNotNull()) + foreach (JsonNode property in item.Where(p => p.Key != "contentTypeKey" && p.Key != "key").Select(p => p.Value).WhereNotNull()) { TraverseProperty(property); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index 98c394d584..97fcd17aa9 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -18,10 +19,6 @@ public abstract class BlockEditorPropertyValueEditor : BlockVal where TLayout : class, IBlockLayoutItem, new() { private readonly IJsonSerializer _jsonSerializer; - private BlockEditorValues? _blockEditorValues; - private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache; - private readonly PropertyEditorCollection _propertyEditors; - private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories; protected BlockEditorPropertyValueEditor( DataEditorAttribute attribute, @@ -32,79 +29,35 @@ public abstract class BlockEditorPropertyValueEditor : BlockVal ILogger> logger, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, - IIOHelper ioHelper) - : base(attribute, propertyEditors, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactories) - { - _propertyEditors = propertyEditors; - _dataValueReferenceFactories = dataValueReferenceFactories; - _dataTypeConfigurationCache = dataTypeConfigurationCache; + IIOHelper ioHelper, + BlockEditorVarianceHandler blockEditorVarianceHandler) + : base(attribute, propertyEditors, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactories, blockEditorVarianceHandler) => _jsonSerializer = jsonSerializer; - } - - protected BlockEditorValues BlockEditorValues - { - get => _blockEditorValues ?? throw new NullReferenceException($"The property {nameof(BlockEditorValues)} must be initialized at value editor construction"); - set => _blockEditorValues = value; - } /// public override IEnumerable GetReferences(object? value) { - // Group by property editor alias to avoid duplicate lookups and optimize value parsing - foreach (var valuesByPropertyEditorAlias in GetAllPropertyValues(value).GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value)) - { - if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor)) - { - continue; - } - - // Use distinct values to avoid duplicate parsing of the same value - foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct())) - { - yield return reference; - } - } + TValue? blockValue = ParseBlockValue(value); + return blockValue is not null + ? GetBlockValueReferences(blockValue) + : Enumerable.Empty(); } /// public override IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) { - foreach (BlockItemData.BlockPropertyValue propertyValue in GetAllPropertyValues(value)) - { - if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) || - dataEditor.GetValueEditor() is not IDataValueTags dataValueTags) - { - continue; - } - - object? configuration = _dataTypeConfigurationCache.GetConfiguration(propertyValue.PropertyType.DataTypeKey); - foreach (ITag tag in dataValueTags.GetTags(propertyValue.Value, configuration, languageId)) - { - yield return tag; - } - } + TValue? blockValue = ParseBlockValue(value); + return blockValue is not null + ? GetBlockValueTags(blockValue, languageId) + : Enumerable.Empty(); } - private IEnumerable GetAllPropertyValues(object? value) + private TValue? ParseBlockValue(object? value) { var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson); - if (blockEditorData is null) - { - yield break; - } - - // Return all property values from the content and settings data - IEnumerable data = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData); - foreach (BlockItemData.BlockPropertyValue propertyValue in data.SelectMany(x => x.PropertyValues.Select(x => x.Value))) - { - yield return propertyValue; - } + return BlockEditorValues.DeserializeAndClean(rawJson)?.BlockValue; } - // note: there is NO variant support here - /// /// Ensure that sub-editor values are translated through their ToEditor methods /// @@ -132,7 +85,7 @@ public abstract class BlockEditorPropertyValueEditor : BlockVal return string.Empty; } - MapBlockValueToEditor(property, blockEditorData.BlockValue); + MapBlockValueToEditor(property, blockEditorData.BlockValue, culture, segment); // return json convertable object return blockEditorData.BlockValue; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs index cb8a69e446..e77ff3d9c0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs @@ -28,6 +28,8 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV new { Path = nameof(BlockValue.SettingsData).ToFirstLowerInvariant(), Items = blockEditorData.BlockValue.SettingsData } }; + var valuesJsonPathPart = nameof(BlockItemData.Values).ToFirstLowerInvariant(); + foreach (var group in itemDataGroups) { var allElementTypes = _elementTypeCache.GetAll(group.Items.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); @@ -40,22 +42,21 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV throw new InvalidOperationException($"No element type found with key {item.ContentTypeKey}"); } - // now ensure missing properties - foreach (IPropertyType elementTypeProp in elementType.CompositionPropertyTypes) - { - if (!item.PropertyValues.ContainsKey(elementTypeProp.Alias)) - { - // set values to null - item.PropertyValues[elementTypeProp.Alias] = new BlockItemData.BlockPropertyValue(null, elementTypeProp); - item.RawPropertyValues[elementTypeProp.Alias] = null; - } - } - + // NOTE: for now this only validates the property data actually sent by the client, not all element properties. + // we need to ensure that all properties for all languages have a matching "item" entry here, to handle validation of + // required properties (see comment in the top of this method). a separate task has been created, get in touch with KJA. var elementValidation = new ElementTypeValidationModel(item.ContentTypeAlias, item.Key); - foreach (KeyValuePair prop in item.PropertyValues) + for (var j = 0; j < item.Values.Count; j++) { + BlockPropertyValue blockPropertyValue = item.Values[j]; + IPropertyType? propertyType = blockPropertyValue.PropertyType; + if (propertyType is null) + { + throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to validate them.", nameof(blockEditorData)); + } + elementValidation.AddPropertyTypeValidation( - new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value, $"{group.Path}[{i}].{prop.Value.PropertyType.Alias}")); + new PropertyTypeValidationModel(propertyType, blockPropertyValue.Value, $"{group.Path}[{i}].{valuesJsonPathPart}[{j}].value")); } yield return elementValidation; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs index 87c78fa119..5fcb8d4dd9 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs @@ -62,7 +62,7 @@ public class BlockEditorValues IDictionary contentTypesDictionary = _elementTypeCache.GetAll(contentTypeKeys).ToDictionary(x=>x.Key); foreach (BlockItemData block in blockEditorData.BlockValue.ContentData.Where(x => - blockEditorData.References.Any(r => x.Udi is not null && r.ContentUdi == x.Udi))) + blockEditorData.References.Any(r => r.ContentKey == x.Key))) { ResolveBlockItemData(block, contentTypePropertyTypes, contentTypesDictionary); } @@ -70,7 +70,7 @@ public class BlockEditorValues // filter out any settings that isn't referenced in the layout references foreach (BlockItemData block in blockEditorData.BlockValue.SettingsData.Where(x => blockEditorData.References.Any(r => - r.SettingsUdi is not null && x.Udi is not null && r.SettingsUdi == x.Udi))) + r.SettingsKey.HasValue && r.SettingsKey.Value == x.Key))) { ResolveBlockItemData(block, contentTypePropertyTypes, contentTypesDictionary); } @@ -82,7 +82,6 @@ public class BlockEditorValues return blockEditorData; } - private bool ResolveBlockItemData(BlockItemData block, Dictionary> contentTypePropertyTypes, IDictionary contentTypesDictionary) { if (contentTypesDictionary.TryGetValue(block.ContentTypeKey, out IContentType? contentType) is false) @@ -97,31 +96,26 @@ public class BlockEditorValues propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); } - var propValues = new Dictionary(); - - // find any keys that are not real property types and remove them - foreach (KeyValuePair prop in block.RawPropertyValues.ToList()) + // resolve the actual property types for all block properties + foreach (BlockPropertyValue property in block.Values) { - // doesn't exist so remove it - if (!propertyTypes.TryGetValue(prop.Key, out IPropertyType? propType)) + if (!propertyTypes.TryGetValue(property.Alias, out IPropertyType? propertyType)) { - block.RawPropertyValues.Remove(prop.Key); _logger.LogWarning( - "The property {PropertyKey} for block {BlockKey} was removed because the property type {PropertyTypeAlias} was not found on {ContentTypeAlias}", - prop.Key, + "The property {PropertyAlias} for block {BlockKey} was removed because the property type was not found on {ContentTypeAlias}", + property.Alias, block.Key, - prop.Key, contentType.Alias); + continue; } - else - { - // set the value to include the resolved property type - propValues[prop.Key] = new BlockItemData.BlockPropertyValue(prop.Value, propType); - } + + property.PropertyType = propertyType; } + // remove all block properties that did not resolve a property type + block.Values.RemoveAll(blockProperty => blockProperty.PropertyType is null); + block.ContentTypeAlias = contentType.Alias; - block.PropertyValues = propValues; return true; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs index 6fe0e3882d..ba1188198a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs @@ -2,6 +2,8 @@ // See LICENSE for more details. using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; @@ -24,6 +26,16 @@ public class BlockGridPropertyEditor : BlockGridPropertyEditorBase public override bool SupportsConfigurableElements => true; + /// + public override bool CanMergePartialPropertyValues(IPropertyType propertyType) => propertyType.VariesByCulture() is false; + + /// + public override object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) + { + var valueEditor = (BlockGridEditorPropertyValueEditor)GetValueEditor(); + return valueEditor.MergePartialPropertyValueForCulture(sourceValue, targetValue, culture); + } + #region Pre Value Editor protected override IConfigurationEditor CreateConfigurationEditor() => new BlockGridConfigurationEditor(_ioHelper); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs index 254933eb8f..2b7364b1aa 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -38,7 +39,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); - private class BlockGridEditorPropertyValueEditor : BlockEditorPropertyValueEditor + internal class BlockGridEditorPropertyValueEditor : BlockEditorPropertyValueEditor { public BlockGridEditorPropertyValueEditor( DataEditorAttribute attribute, @@ -51,14 +52,17 @@ public abstract class BlockGridPropertyEditorBase : DataEditor IJsonSerializer jsonSerializer, IIOHelper ioHelper, IBlockEditorElementTypeCache elementTypeCache, - IPropertyValidationService propertyValidationService) - : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper) + IPropertyValidationService propertyValidationService, + BlockEditorVarianceHandler blockEditorVarianceHandler) + : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper, blockEditorVarianceHandler) { BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), elementTypeCache, logger); Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache)); Validators.Add(new MinMaxValidator(BlockEditorValues, textService)); } + protected override BlockGridValue CreateWithLayout(IEnumerable layout) => new(layout); + private class MinMaxValidator : BlockEditorMinMaxValidatorBase { private readonly BlockEditorValues _blockEditorValues; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs index bbb368dec1..c1a2caacb8 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs @@ -2,7 +2,9 @@ // See LICENSE for more details. using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Serialization; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; @@ -38,6 +40,16 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase public override bool SupportsConfigurableElements => true; + /// + public override bool CanMergePartialPropertyValues(IPropertyType propertyType) => propertyType.VariesByCulture() is false; + + /// + public override object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) + { + var valueEditor = (BlockListEditorPropertyValueEditor)GetValueEditor(); + return valueEditor.MergePartialPropertyValueForCulture(sourceValue, targetValue, culture); + } + #region Pre Value Editor protected override IConfigurationEditor CreateConfigurationEditor() => diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index 2e37056cd2..59fa980d2b 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -62,14 +63,17 @@ public abstract class BlockListPropertyEditorBase : DataEditor IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - IPropertyValidationService propertyValidationService) : - base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper) + IPropertyValidationService propertyValidationService, + BlockEditorVarianceHandler blockEditorVarianceHandler) + : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper, blockEditorVarianceHandler) { BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, elementTypeCache, logger); Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache)); Validators.Add(new MinMaxValidator(BlockEditorValues, textService)); } + protected override BlockListValue CreateWithLayout(IEnumerable layout) => new(layout); + private class MinMaxValidator : BlockEditorMinMaxValidatorBase { private readonly BlockEditorValues _blockEditorValues; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs index c5cbb8d79e..b4521d3bbd 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs @@ -24,8 +24,10 @@ internal sealed class BlockValuePropertyIndexValueFactory : protected override IContentType? GetContentTypeOfNestedItem(BlockItemData input, IDictionary contentTypeDictionary) => contentTypeDictionary.TryGetValue(input.ContentTypeKey, out var result) ? result : null; - protected override IDictionary GetRawProperty(BlockItemData blockItemData) => - blockItemData.RawPropertyValues; + protected override IDictionary GetRawProperty(BlockItemData blockItemData) + => blockItemData.Values + .Where(p => p.Culture is null && p.Segment is null) + .ToDictionary(p => p.Alias, p => p.Value); protected override IEnumerable GetDataItems(IndexValueFactoryBlockValue input) => input.ContentData; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs index f2178fd71a..82d66a3bd0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs @@ -4,9 +4,11 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; @@ -17,7 +19,10 @@ public abstract class BlockValuePropertyValueEditorBase : DataV private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache; private readonly PropertyEditorCollection _propertyEditors; private readonly ILogger _logger; + private readonly IJsonSerializer _jsonSerializer; private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactoryCollection; + private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler; + private BlockEditorValues? _blockEditorValues; protected BlockValuePropertyValueEditorBase( DataEditorAttribute attribute, @@ -28,24 +33,40 @@ public abstract class BlockValuePropertyValueEditorBase : DataV IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection) + DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection, + BlockEditorVarianceHandler blockEditorVarianceHandler) : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute) { _propertyEditors = propertyEditors; _dataTypeConfigurationCache = dataTypeConfigurationCache; _logger = logger; + _jsonSerializer = jsonSerializer; _dataValueReferenceFactoryCollection = dataValueReferenceFactoryCollection; + _blockEditorVarianceHandler = blockEditorVarianceHandler; } /// public abstract IEnumerable GetReferences(object? value); + protected abstract TValue CreateWithLayout(IEnumerable layout); + + protected BlockEditorValues BlockEditorValues + { + get => _blockEditorValues ?? throw new NullReferenceException($"The property {nameof(BlockEditorValues)} must be initialized at value editor construction"); + set => _blockEditorValues = value; + } + protected IEnumerable GetBlockValueReferences(TValue blockValue) { var result = new HashSet(); - BlockItemData.BlockPropertyValue[] propertyValues = blockValue.ContentData.Concat(blockValue.SettingsData) - .SelectMany(x => x.PropertyValues.Values).ToArray(); - foreach (IGrouping valuesByPropertyEditorAlias in propertyValues.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value)) + BlockPropertyValue[] blockPropertyValues = blockValue.ContentData.Concat(blockValue.SettingsData) + .SelectMany(x => x.Values).ToArray(); + if (blockPropertyValues.Any(p => p.PropertyType is null)) + { + throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to find references within them.", nameof(blockValue)); + } + + foreach (IGrouping valuesByPropertyEditorAlias in blockPropertyValues.GroupBy(x => x.PropertyType!.PropertyEditorAlias, x => x.Value)) { if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor)) { @@ -83,9 +104,14 @@ public abstract class BlockValuePropertyValueEditorBase : DataV // loop through all content and settings data foreach (BlockItemData row in blockValue.ContentData.Concat(blockValue.SettingsData)) { - foreach (KeyValuePair prop in row.PropertyValues) + foreach (BlockPropertyValue blockPropertyValue in row.Values) { - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (blockPropertyValue.PropertyType is null) + { + throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to find tags within them.", nameof(blockValue)); + } + + IDataEditor? propEditor = _propertyEditors[blockPropertyValue.PropertyType.PropertyEditorAlias]; IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); if (valueEditor is not IDataValueTags tagsProvider) @@ -93,9 +119,9 @@ public abstract class BlockValuePropertyValueEditorBase : DataV continue; } - object? configuration = _dataTypeConfigurationCache.GetConfiguration(prop.Value.PropertyType.DataTypeKey); + object? configuration = _dataTypeConfigurationCache.GetConfiguration(blockPropertyValue.PropertyType.DataTypeKey); - result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId)); + result.AddRange(tagsProvider.GetTags(blockPropertyValue.Value, configuration, languageId)); } } @@ -108,10 +134,11 @@ public abstract class BlockValuePropertyValueEditorBase : DataV MapBlockItemDataFromEditor(blockValue.SettingsData); } - protected void MapBlockValueToEditor(IProperty property, TValue blockValue) + protected void MapBlockValueToEditor(IProperty property, TValue blockValue, string? culture, string? segment) { - MapBlockItemDataToEditor(property, blockValue.ContentData); - MapBlockItemDataToEditor(property, blockValue.SettingsData); + MapBlockItemDataToEditor(property, blockValue.ContentData, culture, segment); + MapBlockItemDataToEditor(property, blockValue.SettingsData, culture, segment); + _blockEditorVarianceHandler.AlignExposeVariance(blockValue); } protected IEnumerable ConfiguredElementTypeKeys(IBlockConfiguration configuration) @@ -123,71 +150,178 @@ public abstract class BlockValuePropertyValueEditorBase : DataV } } - private void MapBlockItemDataToEditor(IProperty property, List items) + private void MapBlockItemDataToEditor(IProperty property, List items, string? culture, string? segment) { - var valEditors = new Dictionary(); + var valueEditorsByKey = new Dictionary(); - foreach (BlockItemData row in items) + foreach (BlockItemData item in items) { - foreach (KeyValuePair prop in row.PropertyValues) + foreach (BlockPropertyValue blockPropertyValue in item.Values) { - // create a temp property with the value - // - force it to be culture invariant as the block editor can't handle culture variant element properties - prop.Value.PropertyType.Variations = ContentVariation.Nothing; - var tempProp = new Property(prop.Value.PropertyType); - tempProp.SetValue(prop.Value.Value); - - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) + IPropertyType? propertyType = blockPropertyValue.PropertyType; + if (propertyType is null) { - // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. - // if the property editor doesn't exist I think everything will break anyways? - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); + throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to map them to editor.", nameof(items)); + } + + IDataEditor? propertyEditor = _propertyEditors[propertyType.PropertyEditorAlias]; + if (propertyEditor is null) + { + // leave the current block property value as-is - will be used to render a fallback output in the client continue; } - Guid dataTypeKey = prop.Value.PropertyType.DataTypeKey; - if (!valEditors.TryGetValue(dataTypeKey, out IDataValueEditor? valEditor)) - { - var configuration = _dataTypeConfigurationCache.GetConfiguration(dataTypeKey); - valEditor = propEditor.GetValueEditor(configuration); + // if changes were made to the element type variation, we need those changes reflected in the block property values. + // for regular content this happens when a content type is saved (copies of property values are created in the DB), + // but for local block level properties we don't have that kind of handling, so we to do it manually. + // to be friendly we'll map "formerly invariant properties" to the default language ISO code instead of performing a + // hard reset of the property values (which would likely be the most correct thing to do from a data point of view). + _blockEditorVarianceHandler.AlignPropertyVarianceAsync(blockPropertyValue, propertyType, culture).GetAwaiter().GetResult(); - valEditors.Add(dataTypeKey, valEditor); + if (!valueEditorsByKey.TryGetValue(propertyType.DataTypeKey, out IDataValueEditor? valueEditor)) + { + var configuration = _dataTypeConfigurationCache.GetConfiguration(propertyType.DataTypeKey); + valueEditor = propertyEditor.GetValueEditor(configuration); + + valueEditorsByKey.Add(propertyType.DataTypeKey, valueEditor); } - var convValue = valEditor.ToEditor(tempProp); + var tempProp = new Property(propertyType); + tempProp.SetValue(blockPropertyValue.Value, blockPropertyValue.Culture, blockPropertyValue.Segment); + + var editorValue = valueEditor.ToEditor(tempProp, blockPropertyValue.Culture, blockPropertyValue.Segment); // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = convValue; + blockPropertyValue.Value = editorValue; } } } private void MapBlockItemDataFromEditor(List items) { - foreach (BlockItemData row in items) + foreach (BlockItemData item in items) { - foreach (KeyValuePair prop in row.PropertyValues) + foreach (BlockPropertyValue blockPropertyValue in item.Values) { - // Fetch the property types prevalue - var configuration = _dataTypeConfigurationCache.GetConfiguration(prop.Value.PropertyType.DataTypeKey); + IPropertyType? propertyType = blockPropertyValue.PropertyType; + if (propertyType is null) + { + throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to map them from editor.", nameof(items)); + } // Lookup the property editor - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) + IDataEditor? propertyEditor = _propertyEditors[propertyType.PropertyEditorAlias]; + if (propertyEditor is null) { continue; } + // Fetch the property types prevalue + var configuration = _dataTypeConfigurationCache.GetConfiguration(propertyType.DataTypeKey); + // Create a fake content property data object - var contentPropData = new ContentPropertyData(prop.Value.Value, configuration); + var propertyData = new ContentPropertyData(blockPropertyValue.Value, configuration); // Get the property editor to do it's conversion - var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); + var newValue = propertyEditor.GetValueEditor().FromEditor(propertyData, blockPropertyValue.Value); // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = newValue; + blockPropertyValue.Value = newValue; + } + } + } + + internal virtual object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) + { + if (sourceValue is null) + { + return null; + } + + // parse the source value as block editor data + BlockEditorData? sourceBlockEditorValues = BlockEditorValues.DeserializeAndClean(sourceValue); + if (sourceBlockEditorValues?.Layout is null) + { + return null; + } + + // parse the target value as block editor data (fallback to an empty set of block editor data) + BlockEditorData targetBlockEditorValues = + (targetValue is not null ? BlockEditorValues.DeserializeAndClean(targetValue) : null) + ?? new BlockEditorData([], CreateWithLayout(sourceBlockEditorValues.Layout)); + + TValue mergeResult = MergeBlockEditorDataForCulture(sourceBlockEditorValues.BlockValue, targetBlockEditorValues.BlockValue, culture); + return _jsonSerializer.Serialize(mergeResult); + } + + protected TValue MergeBlockEditorDataForCulture(TValue sourceBlockValue, TValue targetBlockValue, string? culture) + { + // structure is global, layout and expose follows structure + targetBlockValue.Layout = sourceBlockValue.Layout; + targetBlockValue.Expose = sourceBlockValue.Expose; + + MergePartialPropertyValueForCulture(sourceBlockValue.ContentData, targetBlockValue.ContentData, culture); + MergePartialPropertyValueForCulture(sourceBlockValue.SettingsData, targetBlockValue.SettingsData, culture); + + return targetBlockValue; + } + + private void MergePartialPropertyValueForCulture(List sourceBlockItems, List targetBlockItems, string? culture) + { + // remove all target blocks that are not part of the source blocks (structure is global) + targetBlockItems.RemoveAll(pb => sourceBlockItems.Any(eb => eb.Key == pb.Key) is false); + + // merge the source values into the target values for culture + foreach (BlockItemData sourceBlockItem in sourceBlockItems) + { + BlockItemData? targetBlockItem = targetBlockItems.FirstOrDefault(i => i.Key == sourceBlockItem.Key); + if (targetBlockItem is null) + { + targetBlockItem = new BlockItemData( + sourceBlockItem.Key, + sourceBlockItem.ContentTypeKey, + sourceBlockItem.ContentTypeAlias); + + // NOTE: this only works because targetBlockItem is by ref! + targetBlockItems.Add(targetBlockItem); + } + + foreach (BlockPropertyValue sourceBlockPropertyValue in sourceBlockItem.Values) + { + // is this another editor that supports partial merging? i.e. blocks within blocks. + IDataEditor? mergingDataEditor = null; + var shouldPerformPartialMerge = sourceBlockPropertyValue.PropertyType is not null + && _propertyEditors.TryGet(sourceBlockPropertyValue.PropertyType.PropertyEditorAlias, out mergingDataEditor) + && mergingDataEditor.CanMergePartialPropertyValues(sourceBlockPropertyValue.PropertyType); + + if (shouldPerformPartialMerge is false && sourceBlockPropertyValue.Culture != culture) + { + // skip for now (irrelevant for the current culture, but might be included in the next pass) + continue; + } + + BlockPropertyValue? targetBlockPropertyValue = targetBlockItem + .Values + .FirstOrDefault(v => + v.Alias == sourceBlockPropertyValue.Alias && + v.Culture == sourceBlockPropertyValue.Culture && + v.Segment == sourceBlockPropertyValue.Segment); + + if (targetBlockPropertyValue is null) + { + targetBlockPropertyValue = new BlockPropertyValue + { + Alias = sourceBlockPropertyValue.Alias, + Culture = sourceBlockPropertyValue.Culture, + Segment = sourceBlockPropertyValue.Segment + }; + targetBlockItem.Values.Add(targetBlockPropertyValue); + } + + // assign source value to target value (or perform partial merge, depending on context) + targetBlockPropertyValue.Value = shouldPerformPartialMerge is false + ? sourceBlockPropertyValue.Value + : mergingDataEditor!.MergePartialPropertyValueForCulture(sourceBlockPropertyValue.Value, targetBlockPropertyValue.Value, culture); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs index e3abf0035f..d99a742c30 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -186,9 +186,6 @@ public class MediaPicker3PropertyEditor : DataEditor private List HandleTemporaryMediaUploads(List mediaWithCropsDtos, MediaPicker3Configuration configuration) { - Guid userKey = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Key - ?? throw new InvalidOperationException("Could not obtain the current backoffice user"); - var invalidDtos = new List(); foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos) @@ -218,7 +215,7 @@ public class MediaPicker3PropertyEditor : DataEditor // there are multiple allowed media types matching the file extension using Stream fileStream = temporaryFile.OpenReadStream(); IMedia mediaFile = _mediaImportService - .ImportAsync(temporaryFile.FileName, fileStream, startNodeGuid, mediaWithCropsDto.MediaTypeAlias, userKey) + .ImportAsync(temporaryFile.FileName, fileStream, startNodeGuid, mediaWithCropsDto.MediaTypeAlias, CurrentUserKey()) .GetAwaiter() .GetResult(); @@ -229,6 +226,9 @@ public class MediaPicker3PropertyEditor : DataEditor return mediaWithCropsDtos.Except(invalidDtos).ToList(); } + private Guid CurrentUserKey() => _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Key + ?? throw new InvalidOperationException("Could not obtain the current backoffice user"); + /// /// Model/DTO that represents the JSON that the MediaPicker3 stores. /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index ef442b768f..b28a99472b 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -6,12 +6,12 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Cache.PropertyEditors; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.PropertyEditors.Validators; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -51,6 +51,16 @@ public class RichTextPropertyEditor : DataEditor public override bool SupportsConfigurableElements => true; + /// + public override bool CanMergePartialPropertyValues(IPropertyType propertyType) => propertyType.VariesByCulture() is false; + + /// + public override object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) + { + var valueEditor = (RichTextPropertyValueEditor)GetValueEditor(); + return valueEditor.MergePartialPropertyValueForCulture(sourceValue, targetValue, culture); + } + /// /// Create a custom value editor /// @@ -78,46 +88,6 @@ public class RichTextPropertyEditor : DataEditor private readonly IRichTextRequiredValidator _richTextRequiredValidator; private readonly ILogger _logger; - [Obsolete("Use non-obsolete constructor. This is schedules for removal in v16.")] - public RichTextPropertyValueEditor( - DataEditorAttribute attribute, - PropertyEditorCollection propertyEditors, - IDataTypeConfigurationCache dataTypeReadCache, - ILogger logger, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - HtmlImageSourceParser imageSourceParser, - HtmlLocalLinkParser localLinkParser, - RichTextEditorPastedImages pastedImages, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - IHtmlSanitizer htmlSanitizer, - IBlockEditorElementTypeCache elementTypeCache, - IPropertyValidationService propertyValidationService, - DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection) - : this( - attribute, - propertyEditors, - dataTypeReadCache, - logger, - backOfficeSecurityAccessor, - localizedTextService, - shortStringHelper, - imageSourceParser, - localLinkParser, - pastedImages, - jsonSerializer, - ioHelper, - htmlSanitizer, - elementTypeCache, - propertyValidationService, - dataValueReferenceFactoryCollection, - StaticServiceProvider.Instance.GetRequiredService()) - { - - } - public RichTextPropertyValueEditor( DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, @@ -135,8 +105,9 @@ public class RichTextPropertyEditor : DataEditor IBlockEditorElementTypeCache elementTypeCache, IPropertyValidationService propertyValidationService, DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection, - IRichTextRequiredValidator richTextRequiredValidator) - : base(attribute, propertyEditors, dataTypeReadCache, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection) + IRichTextRequiredValidator richTextRequiredValidator, + BlockEditorVarianceHandler blockEditorVarianceHandler) + : base(attribute, propertyEditors, dataTypeReadCache, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection, blockEditorVarianceHandler) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _localizedTextService = localizedTextService; @@ -154,6 +125,8 @@ public class RichTextPropertyEditor : DataEditor public override IValueRequiredValidator RequiredValidator => _richTextRequiredValidator; + protected override RichTextBlockValue CreateWithLayout(IEnumerable layout) => new(layout); + /// public override object? ConfigurationObject { @@ -247,7 +220,7 @@ public class RichTextPropertyEditor : DataEditor richTextEditorValue.Markup = _imageSourceParser.EnsureImageSources(richTextEditorValue.Markup); // return json convertable object - return CleanAndMapBlocks(richTextEditorValue, blockValue => MapBlockValueToEditor(property, blockValue)); + return CleanAndMapBlocks(richTextEditorValue, blockValue => MapBlockValueToEditor(property, blockValue, culture, segment)); } /// @@ -295,6 +268,38 @@ public class RichTextPropertyEditor : DataEditor return configuration?.Blocks?.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty(); } + internal override object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) + { + if (sourceValue is null) + { + return null; + } + + if (TryParseEditorValue(sourceValue, out RichTextEditorValue? sourceRichTextEditorValue) is false + || sourceRichTextEditorValue.Blocks is null) + { + return null; + } + + BlockEditorData? sourceBlockEditorData = ConvertAndClean(sourceRichTextEditorValue.Blocks); + if (sourceBlockEditorData?.Layout is null) + { + return null; + } + + TryParseEditorValue(targetValue, out RichTextEditorValue? targetRichTextEditorValue); + + BlockEditorData targetBlockEditorData = + (targetRichTextEditorValue?.Blocks is not null ? ConvertAndClean(targetRichTextEditorValue.Blocks) : null) + ?? new BlockEditorData([], CreateWithLayout(sourceBlockEditorData.Layout)); + + RichTextBlockValue blocksMergeResult = MergeBlockEditorDataForCulture(sourceBlockEditorData.BlockValue, targetBlockEditorData.BlockValue, culture); + + // structure is global, and markup follows structure + var mergedEditorValue = new RichTextEditorValue { Markup = sourceRichTextEditorValue.Markup, Blocks = blocksMergeResult }; + return RichTextPropertyEditorHelper.SerializeRichTextEditorValue(mergedEditorValue, _jsonSerializer); + } + private bool TryParseEditorValue(object? value, [NotNullWhen(true)] out RichTextEditorValue? richTextEditorValue) => RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out richTextEditorValue); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyIndexValueFactory.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyIndexValueFactory.cs index b89b36f99e..e2236e2976 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyIndexValueFactory.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyIndexValueFactory.cs @@ -71,7 +71,9 @@ internal class RichTextPropertyIndexValueFactory : NestedPropertyIndexValueFacto => contentTypeDictionary.TryGetValue(nestedItem.ContentTypeKey, out var result) ? result : null; protected override IDictionary GetRawProperty(BlockItemData blockItemData) - => blockItemData.RawPropertyValues; + => blockItemData.Values + .Where(p => p.Culture is null && p.Segment is null) + .ToDictionary(p => p.Alias, p => p.Value); protected override IEnumerable GetDataItems(RichTextEditorValue input) => input.Blocks?.ContentData ?? new List(); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs index f84e1e31b2..2dc70eea0d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs @@ -15,16 +15,22 @@ public sealed class BlockEditorConverter { private readonly IPublishedModelFactory _publishedModelFactory; private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler; public BlockEditorConverter( IPublishedSnapshotAccessor publishedSnapshotAccessor, - IPublishedModelFactory publishedModelFactory) + IPublishedModelFactory publishedModelFactory, + IVariationContextAccessor variationContextAccessor, + BlockEditorVarianceHandler blockEditorVarianceHandler) { _publishedSnapshotAccessor = publishedSnapshotAccessor; _publishedModelFactory = publishedModelFactory; + _variationContextAccessor = variationContextAccessor; + _blockEditorVarianceHandler = blockEditorVarianceHandler; } - public IPublishedElement? ConvertToElement(BlockItemData data, PropertyCacheLevel referenceCacheLevel, bool preview) + public IPublishedElement? ConvertToElement(IPublishedElement owner, BlockItemData data, PropertyCacheLevel referenceCacheLevel, bool preview) { IPublishedContentCache? publishedContentCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Content; @@ -36,10 +42,45 @@ public sealed class BlockEditorConverter return null; } - Dictionary propertyValues = data.RawPropertyValues; + VariationContext variationContext = _variationContextAccessor.VariationContext ?? new VariationContext(); - // Get the UDI from the deserialized object. If this is empty, we can fallback to checking the 'key' if there is one - Guid key = data.Udi is GuidUdi gudi ? gudi.Guid : Guid.Empty; + var propertyTypesByAlias = publishedContentType + .PropertyTypes + .ToDictionary(propertyType => propertyType.Alias); + + var propertyValues = new Dictionary(); + foreach (BlockPropertyValue property in data.Values) + { + if (!propertyTypesByAlias.TryGetValue(property.Alias, out IPublishedPropertyType? propertyType)) + { + continue; + } + + // if case changes have been made to the content or element type variation since the parent content was published, + // we need to align those changes for the block properties - unlike for root level properties, where these + // things are handled when a content type is saved. + BlockPropertyValue? alignedProperty = _blockEditorVarianceHandler.AlignedPropertyVarianceAsync(property, propertyType, owner).GetAwaiter().GetResult(); + if (alignedProperty is null) + { + continue; + } + + var expectedCulture = owner.ContentType.VariesByCulture() && publishedContentType.VariesByCulture() && propertyType.VariesByCulture() + ? variationContext.Culture + : null; + var expectedSegment = owner.ContentType.VariesBySegment() && publishedContentType.VariesBySegment() && propertyType.VariesBySegment() + ? variationContext.Segment + : null; + + if (alignedProperty.Culture.NullOrWhiteSpaceAsNull().InvariantEquals(expectedCulture.NullOrWhiteSpaceAsNull()) + && alignedProperty.Segment.NullOrWhiteSpaceAsNull().InvariantEquals(expectedSegment.NullOrWhiteSpaceAsNull())) + { + propertyValues[alignedProperty.Alias] = alignedProperty.Value; + } + } + + // Get the key from the deserialized object. If this is empty, we can fallback to checking the 'key' if there is one + Guid key = data.Key; if (key == Guid.Empty && propertyValues.TryGetValue("key", out var keyo)) { Guid.TryParse(keyo!.ToString(), out key); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorPropertyValueConstructorCacheBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorPropertyValueConstructorCacheBase.cs index 3af7b4beba..df722c611a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorPropertyValueConstructorCacheBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorPropertyValueConstructorCacheBase.cs @@ -9,13 +9,13 @@ public abstract class BlockEditorPropertyValueConstructorCacheBase where T : IBlockReference { private readonly - ConcurrentDictionary<(Guid, Guid?), Func> + ConcurrentDictionary<(Guid, Guid?), Func> _constructorCache = new(); - public bool TryGetValue((Guid ContentTypeKey, Guid? SettingsTypeKey) key, [MaybeNullWhen(false)] out Func value) + public bool TryGetValue((Guid ContentTypeKey, Guid? SettingsTypeKey) key, [MaybeNullWhen(false)] out Func value) => _constructorCache.TryGetValue(key, out value); - public void SetValue((Guid ContentTypeKey, Guid? SettingsTypeKey) key, Func value) + public void SetValue((Guid ContentTypeKey, Guid? SettingsTypeKey) key, Func value) => _constructorCache[key] = value; public void Clear() diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorVarianceHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorVarianceHandler.cs new file mode 100644 index 0000000000..0034648191 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorVarianceHandler.cs @@ -0,0 +1,137 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +public sealed class BlockEditorVarianceHandler +{ + private readonly ILanguageService _languageService; + + public BlockEditorVarianceHandler(ILanguageService languageService) + => _languageService = languageService; + + public async Task AlignPropertyVarianceAsync(BlockPropertyValue blockPropertyValue, IPropertyType propertyType, string? culture) + { + culture ??= await _languageService.GetDefaultIsoCodeAsync(); + if (propertyType.VariesByCulture() != VariesByCulture(blockPropertyValue)) + { + blockPropertyValue.Culture = propertyType.VariesByCulture() + ? culture + : null; + } + } + + public async Task AlignedPropertyVarianceAsync(BlockPropertyValue blockPropertyValue, IPublishedPropertyType propertyType, IPublishedElement owner) + { + ContentVariation propertyTypeVariation = owner.ContentType.Variations & propertyType.Variations; + if (propertyTypeVariation.VariesByCulture() == VariesByCulture(blockPropertyValue)) + { + return blockPropertyValue; + } + + // mismatch in culture variation for published content: + // - if the property type varies by culture, assign the default culture + // - if the property type does not vary by culture: + // - if the property value culture equals the default culture, assign a null value for it to be rendered as the invariant value + // - otherwise return null (not applicable for rendering) + var defaultCulture = await _languageService.GetDefaultIsoCodeAsync(); + if (propertyTypeVariation.VariesByCulture()) + { + return new BlockPropertyValue + { + Alias = blockPropertyValue.Alias, + Culture = defaultCulture, + Segment = blockPropertyValue.Segment, + Value = blockPropertyValue.Value, + PropertyType = blockPropertyValue.PropertyType + }; + } + + if (defaultCulture.Equals(blockPropertyValue.Culture)) + { + return new BlockPropertyValue + { + Alias = blockPropertyValue.Alias, + Culture = null, + Segment = blockPropertyValue.Segment, + Value = blockPropertyValue.Value, + PropertyType = blockPropertyValue.PropertyType + }; + } + + return null; + } + + public async Task> AlignedExposeVarianceAsync(BlockValue blockValue, IPublishedElement owner, IPublishedElement element) + { + BlockItemVariation[] blockVariations = blockValue.Expose.Where(v => v.ContentKey == element.Key).ToArray(); + if (blockVariations.Any() is false) + { + return blockVariations; + } + + // in case of mismatch in culture variation for block value variation: + // - if the expected variation is by culture, assign the default culture to all block variation + // - if the expected variation is not by culture, use all in block variation from the default culture as invariant + + ContentVariation exposeVariation = owner.ContentType.Variations & element.ContentType.Variations; + if (exposeVariation.VariesByCulture() && blockVariations.All(v => v.Culture is null)) + { + var defaultCulture = await _languageService.GetDefaultIsoCodeAsync(); + return blockVariations.Select(v => new BlockItemVariation(v.ContentKey, defaultCulture, v.Segment)); + } + + if (exposeVariation.VariesByCulture() is false && blockVariations.All(v => v.Culture is not null)) + { + var defaultCulture = await _languageService.GetDefaultIsoCodeAsync(); + return blockVariations + .Where(v => v.Culture == defaultCulture) + .Select(v => new BlockItemVariation(v.ContentKey, null, v.Segment)) + .ToList(); + } + + return blockVariations; + } + + public void AlignExposeVariance(BlockValue blockValue) + { + var contentDataToAlign = new List(); + foreach (BlockItemVariation variation in blockValue.Expose) + { + BlockItemData? contentData = blockValue.ContentData.FirstOrDefault(cd => cd.Key == variation.ContentKey); + if (contentData is null) + { + continue; + } + + if((variation.Culture is null && contentData.Values.Any(v => v.Culture is not null)) + || (variation.Culture is not null && contentData.Values.All(v => v.Culture is null))) + { + contentDataToAlign.Add(contentData); + } + } + + if (contentDataToAlign.Any() is false) + { + return; + } + + blockValue.Expose.RemoveAll(v => contentDataToAlign.Any(cd => cd.Key == v.ContentKey)); + foreach (BlockItemData contentData in contentDataToAlign) + { + var omitNullCulture = contentData.Values.Any(v => v.Culture is not null); + foreach (BlockPropertyValue value in contentData.Values + .Where(v => omitNullCulture is false || v.Culture is not null) + .DistinctBy(v => v.Culture + v.Segment)) + { + blockValue.Expose.Add(new BlockItemVariation(contentData.Key, value.Culture, value.Segment)); + } + } + } + + private static bool VariesByCulture(BlockPropertyValue blockPropertyValue) + => blockPropertyValue.Culture.IsNullOrWhiteSpace() is false; +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs index 1e00b5ee6f..822632aaf3 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs @@ -22,14 +22,19 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters private readonly IJsonSerializer _jsonSerializer; private readonly IApiElementBuilder _apiElementBuilder; private readonly BlockGridPropertyValueConstructorCache _constructorCache; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler; - [Obsolete("Please use non-obsolete construtor. This will be removed in Umbraco 15.")] + [Obsolete("Use the constructor that takes all parameters, scheduled for removal in V16")] public BlockGridPropertyValueConverter( IProfilingLogger proflog, BlockEditorConverter blockConverter, IJsonSerializer jsonSerializer, - IApiElementBuilder apiElementBuilder) - : this(proflog, blockConverter, jsonSerializer, apiElementBuilder, StaticServiceProvider.Instance.GetRequiredService()) + IApiElementBuilder apiElementBuilder, + BlockGridPropertyValueConstructorCache constructorCache) + : this(proflog, blockConverter, jsonSerializer, apiElementBuilder, constructorCache, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -38,13 +43,17 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters BlockEditorConverter blockConverter, IJsonSerializer jsonSerializer, IApiElementBuilder apiElementBuilder, - BlockGridPropertyValueConstructorCache constructorCache) + BlockGridPropertyValueConstructorCache constructorCache, + IVariationContextAccessor variationContextAccessor, + BlockEditorVarianceHandler blockEditorVarianceHandler) { _proflog = proflog; _blockConverter = blockConverter; _jsonSerializer = jsonSerializer; _apiElementBuilder = apiElementBuilder; _constructorCache = constructorCache; + _variationContextAccessor = variationContextAccessor; + _blockEditorVarianceHandler = blockEditorVarianceHandler; } /// @@ -61,7 +70,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - => ConvertIntermediateToBlockGridModel(propertyType, referenceCacheLevel, inter, preview); + => ConvertIntermediateToBlockGridModel(owner, propertyType, referenceCacheLevel, inter, preview); /// public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => GetPropertyCacheLevel(propertyType); @@ -78,7 +87,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters { const int defaultColumns = 12; - BlockGridModel? blockGridModel = ConvertIntermediateToBlockGridModel(propertyType, referenceCacheLevel, inter, preview); + BlockGridModel? blockGridModel = ConvertIntermediateToBlockGridModel(owner, propertyType, referenceCacheLevel, inter, preview); if (blockGridModel == null) { return new ApiBlockGridModel(defaultColumns, Array.Empty()); @@ -109,7 +118,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters return model; } - private BlockGridModel? ConvertIntermediateToBlockGridModel(IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + private BlockGridModel? ConvertIntermediateToBlockGridModel(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) { using (!_proflog.IsEnabled(LogLevel.Debug) ? null : _proflog.DebugDuration($"ConvertPropertyToBlockGrid ({propertyType.DataType.Id})")) { @@ -132,8 +141,8 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters return null; } - var creator = new BlockGridPropertyValueCreator(_blockConverter, _jsonSerializer, _constructorCache); - return creator.CreateBlockModel(referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks, configuration.GridColumns); + var creator = new BlockGridPropertyValueCreator(_blockConverter, _variationContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache); + return creator.CreateBlockModel(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks, configuration.GridColumns); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueCreator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueCreator.cs index 6b1252f751..1cfef58b23 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueCreator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueCreator.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; @@ -9,14 +10,19 @@ internal class BlockGridPropertyValueCreator : BlockPropertyValueCreatorBase BlockGridModel.Empty; @@ -46,6 +52,7 @@ internal class BlockGridPropertyValueCreator : BlockPropertyValueCreatorBase(), StaticServiceProvider.Instance.GetRequiredService()) + [Obsolete("Use the constructor that takes all parameters, scheduled for removal in V16")] + public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter, IContentTypeService contentTypeService, IApiElementBuilder apiElementBuilder, IJsonSerializer jsonSerializer, BlockListPropertyValueConstructorCache constructorCache) + : this(proflog, blockConverter, contentTypeService, apiElementBuilder, jsonSerializer, constructorCache, StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()) { } - [Obsolete("Use the constructor that takes all parameters, scheduled for removal in V15")] - public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter, IContentTypeService contentTypeService, IApiElementBuilder apiElementBuilder, BlockListPropertyValueConstructorCache constructorCache) - : this(proflog, blockConverter, contentTypeService, apiElementBuilder, StaticServiceProvider.Instance.GetRequiredService(), constructorCache) - { - } - - public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter, IContentTypeService contentTypeService, IApiElementBuilder apiElementBuilder, IJsonSerializer jsonSerializer, BlockListPropertyValueConstructorCache constructorCache) + public BlockListPropertyValueConverter( + IProfilingLogger proflog, + BlockEditorConverter blockConverter, + IContentTypeService contentTypeService, + IApiElementBuilder apiElementBuilder, + IJsonSerializer jsonSerializer, + BlockListPropertyValueConstructorCache constructorCache, + IVariationContextAccessor variationContextAccessor, + BlockEditorVarianceHandler blockEditorVarianceHandler) { _proflog = proflog; _blockConverter = blockConverter; @@ -48,6 +51,8 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli _apiElementBuilder = apiElementBuilder; _jsonSerializer = jsonSerializer; _constructorCache = constructorCache; + _variationContextAccessor = variationContextAccessor; + _blockEditorVarianceHandler = blockEditorVarianceHandler; } /// @@ -159,8 +164,8 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli return null; } - var creator = new BlockListPropertyValueCreator(_blockConverter, _jsonSerializer, _constructorCache); - return creator.CreateBlockModel(referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks); + var creator = new BlockListPropertyValueCreator(_blockConverter, _variationContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache); + return creator.CreateBlockModel(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueCreator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueCreator.cs index 853dc1027f..5714256759 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueCreator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueCreator.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; @@ -10,21 +11,23 @@ internal class BlockListPropertyValueCreator : BlockPropertyValueCreatorBase BlockListModel.Empty; BlockListModel CreateModel(IList items) => new BlockListModel(items); - BlockListModel blockModel = CreateBlockModel(referenceCacheLevel, intermediateBlockModelValue, preview, blockConfigurations, CreateEmptyModel, CreateModel); + BlockListModel blockModel = CreateBlockModel(owner, referenceCacheLevel, intermediateBlockModelValue, preview, blockConfigurations, CreateEmptyModel, CreateModel); return blockModel; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueConverterBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueConverterBase.cs deleted file mode 100644 index dd0d206a78..0000000000 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueConverterBase.cs +++ /dev/null @@ -1,255 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System.Reflection; -using Umbraco.Cms.Core.Models.Blocks; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; - -[Obsolete("Please use implementations of BlockPropertyValueCreatorBase instead of this. See BlockListPropertyValueConverter for inspiration.. Will be removed in V15.")] -public abstract class BlockPropertyValueConverterBase : PropertyValueConverterBase - where TBlockItemModel : class, IBlockReference - where TBlockLayoutItem : class, IBlockLayoutItem, new() - where TBlockConfiguration : IBlockConfiguration - where TBlockValue : BlockValue, new() -{ - /// - /// Creates a specific data converter for the block property implementation. - /// - /// - protected abstract BlockEditorDataConverter CreateBlockEditorDataConverter(); - - /// - /// Creates a specific block item activator for the block property implementation. - /// - /// - protected abstract BlockItemActivator CreateBlockItemActivator(); - - /// - /// Creates an empty block model, i.e. for uninitialized or invalid property values. - /// - /// - protected delegate TBlockModel CreateEmptyBlockModel(); - - /// - /// Creates a block model for a list of unwrapped block items. - /// - /// The unwrapped block items to base the block model on. - /// - protected delegate TBlockModel CreateBlockModel(IList blockItems); - - /// - /// Creates a block item from a block layout item. - /// - /// The block layout item to base the block item on. - /// - protected delegate TBlockItemModel? CreateBlockItemModelFromLayout(TBlockLayoutItem layoutItem); - - /// - /// Enriches a block item after it has been created by the block item activator. Use this to set block item data based on concrete block layout and configuration. - /// - /// The block item to enrich. - /// The block layout item for the block item being enriched. - /// The configuration of the block. - /// Delegate for creating new block items from block layout items. - /// - protected delegate TBlockItemModel? EnrichBlockItemModelFromConfiguration(TBlockItemModel item, TBlockLayoutItem layoutItem, TBlockConfiguration configuration, CreateBlockItemModelFromLayout blockItemModelCreator); - - protected BlockPropertyValueConverterBase(BlockEditorConverter blockBlockEditorConverter) => BlockEditorConverter = blockBlockEditorConverter; - - protected BlockEditorConverter BlockEditorConverter { get; } - - /// - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) => source?.ToString(); - - /// - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(TBlockModel); - - /// - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - protected TBlockModel UnwrapBlockModel( - PropertyCacheLevel referenceCacheLevel, - object? inter, - bool preview, - IEnumerable blockConfigurations, - CreateEmptyBlockModel createEmptyModel, - CreateBlockModel createModel, - EnrichBlockItemModelFromConfiguration? enrichBlockItem = null) - { - // NOTE: The intermediate object is just a json string, we don't actually convert from source -> intermediate since source is always just a json string - - var value = (string?)inter; - - // Short-circuit on empty values - if (string.IsNullOrWhiteSpace(value)) - { - return createEmptyModel(); - } - - BlockEditorDataConverter blockEditorDataConverter = CreateBlockEditorDataConverter(); - BlockEditorData converted = blockEditorDataConverter.Deserialize(value); - if (converted.BlockValue.ContentData.Count == 0) - { - return createEmptyModel(); - } - - IEnumerable? layout = converted.Layout; - if (layout is null) - { - return createEmptyModel(); - } - - var blockConfigMap = blockConfigurations.ToDictionary(bc => bc.ContentElementTypeKey); - - // Convert the content data - var contentPublishedElements = new Dictionary(); - foreach (BlockItemData data in converted.BlockValue.ContentData) - { - if (!blockConfigMap.ContainsKey(data.ContentTypeKey)) - { - continue; - } - - IPublishedElement? element = BlockEditorConverter.ConvertToElement(data, referenceCacheLevel, preview); - if (element == null) - { - continue; - } - - contentPublishedElements[element.Key] = element; - } - - // If there are no content elements, it doesn't matter what is stored in layout - if (contentPublishedElements.Count == 0) - { - return createEmptyModel(); - } - - // Convert the settings data - var settingsPublishedElements = new Dictionary(); - var validSettingsElementTypes = blockConfigMap.Values.Select(x => x.SettingsElementTypeKey) - .Where(x => x.HasValue).Distinct().ToList(); - foreach (BlockItemData data in converted.BlockValue.SettingsData) - { - if (!validSettingsElementTypes.Contains(data.ContentTypeKey)) - { - continue; - } - - IPublishedElement? element = BlockEditorConverter.ConvertToElement(data, referenceCacheLevel, preview); - if (element is null) - { - continue; - } - - settingsPublishedElements[element.Key] = element; - } - - BlockItemActivator blockItemActivator = CreateBlockItemActivator(); - - TBlockItemModel? CreateBlockItem(TBlockLayoutItem layoutItem) - { - // Get the content reference - var contentGuidUdi = (GuidUdi?)layoutItem.ContentUdi; - if (contentGuidUdi is null || - !contentPublishedElements.TryGetValue(contentGuidUdi.Guid, out IPublishedElement? contentData)) - { - return null; - } - - if (!blockConfigMap.TryGetValue( - contentData.ContentType.Key, - out TBlockConfiguration? blockConfig)) - { - return null; - } - - // Get the setting reference - IPublishedElement? settingsData = null; - var settingGuidUdi = (GuidUdi?)layoutItem.SettingsUdi; - if (settingGuidUdi is not null) - { - settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData); - } - - // This can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again - // We also ensure that the content types match, since maybe the settings type has been changed after this has been persisted - if (settingsData is not null && (!blockConfig.SettingsElementTypeKey.HasValue || - settingsData.ContentType.Key != blockConfig.SettingsElementTypeKey)) - { - settingsData = null; - } - - // Create instance (use content/settings type from configuration) - TBlockItemModel? blockItem = blockItemActivator.CreateInstance(blockConfig.ContentElementTypeKey, blockConfig.SettingsElementTypeKey, contentGuidUdi, contentData, settingGuidUdi, settingsData); - if (blockItem == null) - { - return null; - } - - if (enrichBlockItem != null) - { - blockItem = enrichBlockItem(blockItem, layoutItem, blockConfig, CreateBlockItem); - } - - return blockItem; - } - - var blockItems = layout.Select(CreateBlockItem).WhereNotNull().ToList(); - return createModel(blockItems); - } - - // Cache constructors locally (it's tied to the current IPublishedSnapshot and IPublishedModelFactory) - protected abstract class BlockItemActivator - { - protected abstract Type GenericItemType { get; } - - private readonly BlockEditorConverter _blockConverter; - - private readonly - Dictionary<(Guid, Guid?), Func> - _constructorCache = new(); - - public BlockItemActivator(BlockEditorConverter blockConverter) - => _blockConverter = blockConverter; - - public T CreateInstance(Guid contentTypeKey, Guid? settingsTypeKey, Udi contentUdi, IPublishedElement contentData, Udi? settingsUdi, IPublishedElement? settingsData) - { - if (!_constructorCache.TryGetValue( - (contentTypeKey, settingsTypeKey), - out Func? constructor)) - { - constructor = _constructorCache[(contentTypeKey, settingsTypeKey)] = - EmitConstructor(contentTypeKey, settingsTypeKey); - } - - return constructor(contentUdi, contentData, settingsUdi, settingsData); - } - - private Func EmitConstructor( - Guid contentTypeKey, Guid? settingsTypeKey) - { - Type contentType = _blockConverter.GetModelType(contentTypeKey); - Type settingsType = settingsTypeKey.HasValue - ? _blockConverter.GetModelType(settingsTypeKey.Value) - : typeof(IPublishedElement); - Type type = GenericItemType.MakeGenericType(contentType, settingsType); - - ConstructorInfo? constructor = - type.GetConstructor(new[] { typeof(Udi), contentType, typeof(Udi), settingsType }); - if (constructor == null) - { - throw new InvalidOperationException($"Could not find the required public constructor on {type}."); - } - - // We use unsafe here, because we know the constructor parameter count and types match - return ReflectionUtilities - .EmitConstructorUnsafe>( - constructor); - } - } -} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueCreatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueCreatorBase.cs index 45d4cdff2b..b7dca91515 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueCreatorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueCreatorBase.cs @@ -15,6 +15,9 @@ internal abstract class BlockPropertyValueCreatorBase, new() { + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler; + /// /// Creates a specific data converter for the block property implementation. /// @@ -57,11 +60,17 @@ internal abstract class BlockPropertyValueCreatorBase protected delegate TBlockItemModel? EnrichBlockItemModelFromConfiguration(TBlockItemModel item, TBlockLayoutItem layoutItem, TBlockConfiguration configuration, CreateBlockItemModelFromLayout blockItemModelCreator); - protected BlockPropertyValueCreatorBase(BlockEditorConverter blockEditorConverter) => BlockEditorConverter = blockEditorConverter; + protected BlockPropertyValueCreatorBase(BlockEditorConverter blockEditorConverter, IVariationContextAccessor variationContextAccessor, BlockEditorVarianceHandler blockEditorVarianceHandler) + { + BlockEditorConverter = blockEditorConverter; + _variationContextAccessor = variationContextAccessor; + _blockEditorVarianceHandler = blockEditorVarianceHandler; + } protected BlockEditorConverter BlockEditorConverter { get; } protected TBlockModel CreateBlockModel( + IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, string intermediateBlockModelValue, bool preview, @@ -78,10 +87,11 @@ internal abstract class BlockPropertyValueCreatorBase blockEditorDataConverter = CreateBlockEditorDataConverter(); BlockEditorData converted = blockEditorDataConverter.Deserialize(intermediateBlockModelValue); - return CreateBlockModel(referenceCacheLevel, converted, preview, blockConfigurations, createEmptyModel, createModelFromItems, enrichBlockItem); + return CreateBlockModel(owner, referenceCacheLevel, converted, preview, blockConfigurations, createEmptyModel, createModelFromItems, enrichBlockItem); } protected TBlockModel CreateBlockModel( + IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, TBlockValue blockValue, bool preview, @@ -92,10 +102,11 @@ internal abstract class BlockPropertyValueCreatorBase blockEditorDataConverter = CreateBlockEditorDataConverter(); BlockEditorData converted = blockEditorDataConverter.Convert(blockValue); - return CreateBlockModel(referenceCacheLevel, converted, preview, blockConfigurations, createEmptyModel, createModelFromItems, enrichBlockItem); + return CreateBlockModel(owner, referenceCacheLevel, converted, preview, blockConfigurations, createEmptyModel, createModelFromItems, enrichBlockItem); } private TBlockModel CreateBlockModel( + IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, BlockEditorData converted, bool preview, @@ -115,6 +126,7 @@ internal abstract class BlockPropertyValueCreatorBase bc.ContentElementTypeKey); + VariationContext variationContext = _variationContextAccessor.VariationContext ?? new VariationContext(); // Convert the content data var contentPublishedElements = new Dictionary(); @@ -125,12 +137,28 @@ internal abstract class BlockPropertyValueCreatorBase expose = _blockEditorVarianceHandler.AlignedExposeVarianceAsync(converted.BlockValue, owner, element).GetAwaiter().GetResult(); + var expectedBlockVariationCulture = owner.ContentType.VariesByCulture() && element.ContentType.VariesByCulture() + ? variationContext.Culture.NullOrWhiteSpaceAsNull() + : null; + var expectedBlockVariationSegment = owner.ContentType.VariesBySegment() && element.ContentType.VariesBySegment() + ? variationContext.Segment.NullOrWhiteSpaceAsNull() + : null; + if (expose.Any(v => + v.ContentKey == element.Key && v.Culture == expectedBlockVariationCulture && + v.Segment == expectedBlockVariationSegment) is false) + { + continue; + } + contentPublishedElements[element.Key] = element; } @@ -151,7 +179,7 @@ internal abstract class BlockPropertyValueCreatorBase? constructor)) + out Func? constructor)) { constructor = EmitConstructor(contentTypeKey, settingsTypeKey); _constructorCache.SetValue((contentTypeKey, settingsTypeKey), constructor); } - return constructor(contentUdi, contentData, settingsUdi, settingsData); + return constructor(contentKey, contentData, settingsKey, settingsData); } - private Func EmitConstructor( + private Func EmitConstructor( Guid contentTypeKey, Guid? settingsTypeKey) { Type contentType = _blockConverter.GetModelType(contentTypeKey); @@ -253,7 +278,7 @@ internal abstract class BlockPropertyValueCreatorBase>( + .EmitConstructorUnsafe>( constructor); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs index 9ec91fa0de..241b5825e2 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; @@ -13,21 +14,23 @@ internal class RichTextBlockPropertyValueCreator : BlockPropertyValueCreatorBase public RichTextBlockPropertyValueCreator( BlockEditorConverter blockEditorConverter, + IVariationContextAccessor variationContextAccessor, + BlockEditorVarianceHandler blockEditorVarianceHandler, IJsonSerializer jsonSerializer, RichTextBlockPropertyValueConstructorCache constructorCache) - : base(blockEditorConverter) + : base(blockEditorConverter, variationContextAccessor, blockEditorVarianceHandler) { _jsonSerializer = jsonSerializer; _constructorCache = constructorCache; } - public RichTextBlockModel CreateBlockModel(PropertyCacheLevel referenceCacheLevel, RichTextBlockValue blockValue, bool preview, RichTextConfiguration.RichTextBlockConfiguration[] blockConfigurations) + public RichTextBlockModel CreateBlockModel(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, RichTextBlockValue blockValue, bool preview, RichTextConfiguration.RichTextBlockConfiguration[] blockConfigurations) { RichTextBlockModel CreateEmptyModel() => RichTextBlockModel.Empty; RichTextBlockModel CreateModel(IList items) => new RichTextBlockModel(items); - RichTextBlockModel blockModel = CreateBlockModel(referenceCacheLevel, blockValue, preview, blockConfigurations, CreateEmptyModel, CreateModel); + RichTextBlockModel blockModel = CreateBlockModel(owner, referenceCacheLevel, blockValue, preview, blockConfigurations, CreateEmptyModel, CreateModel); return blockModel; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs index 2d4c19c17a..1041b6922d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs @@ -4,6 +4,6 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; internal static partial class RichTextParsingRegexes { - [GeneratedRegex(".[^\"]*)\">(?:)?<\\/umb-rte-block(?:-inline)?>")] + [GeneratedRegex(".[^\"]*)\">(?:)?<\\/umb-rte-block(?:-inline)?>")] public static partial Regex BlockRegex(); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs index 8a0ec3ba4a..58bf1bf05b 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Text.RegularExpressions; using HtmlAgilityPack; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Blocks; @@ -13,6 +14,7 @@ using Umbraco.Cms.Core.PropertyEditors.DeliveryApi; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Serialization; @@ -39,13 +41,27 @@ public class RteBlockRenderingValueConverter : SimpleTinyMceValueConverter, IDel private readonly ILogger _logger; private readonly IApiElementBuilder _apiElementBuilder; private readonly RichTextBlockPropertyValueConstructorCache _constructorCache; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler; private DeliveryApiSettings _deliveryApiSettings; + [Obsolete("Use the constructor that takes all parameters, scheduled for removal in V16")] public RteBlockRenderingValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser, IApiRichTextElementParser apiRichTextElementParser, IApiRichTextMarkupParser apiRichTextMarkupParser, IPartialViewBlockEngine partialViewBlockEngine, BlockEditorConverter blockEditorConverter, IJsonSerializer jsonSerializer, IApiElementBuilder apiElementBuilder, RichTextBlockPropertyValueConstructorCache constructorCache, ILogger logger, IOptionsMonitor deliveryApiSettingsMonitor) + : this(linkParser, urlParser, imageSourceParser, apiRichTextElementParser, apiRichTextMarkupParser, partialViewBlockEngine, blockEditorConverter, jsonSerializer, + apiElementBuilder, constructorCache, logger, StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), deliveryApiSettingsMonitor) + { + } + + public RteBlockRenderingValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser, + IApiRichTextElementParser apiRichTextElementParser, IApiRichTextMarkupParser apiRichTextMarkupParser, + IPartialViewBlockEngine partialViewBlockEngine, BlockEditorConverter blockEditorConverter, IJsonSerializer jsonSerializer, + IApiElementBuilder apiElementBuilder, RichTextBlockPropertyValueConstructorCache constructorCache, ILogger logger, + IVariationContextAccessor variationContextAccessor, BlockEditorVarianceHandler blockEditorVarianceHandler, IOptionsMonitor deliveryApiSettingsMonitor) { _linkParser = linkParser; _urlParser = urlParser; @@ -58,6 +74,8 @@ public class RteBlockRenderingValueConverter : SimpleTinyMceValueConverter, IDel _apiElementBuilder = apiElementBuilder; _constructorCache = constructorCache; _logger = logger; + _variationContextAccessor = variationContextAccessor; + _blockEditorVarianceHandler = blockEditorVarianceHandler; _deliveryApiSettings = deliveryApiSettingsMonitor.CurrentValue; deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings); } @@ -78,7 +96,7 @@ public class RteBlockRenderingValueConverter : SimpleTinyMceValueConverter, IDel // the reference cache level is .Element here, as is also the case when rendering at property level. RichTextBlockModel? richTextBlockModel = richTextEditorValue.Blocks is not null - ? ParseRichTextBlockModel(richTextEditorValue.Blocks, propertyType, PropertyCacheLevel.Element, preview) + ? ParseRichTextBlockModel(owner, richTextEditorValue.Blocks, propertyType, PropertyCacheLevel.Element, preview) : null; return new RichTextEditorIntermediateValue @@ -181,7 +199,7 @@ public class RteBlockRenderingValueConverter : SimpleTinyMceValueConverter, IDel return sourceString; } - private RichTextBlockModel? ParseRichTextBlockModel(RichTextBlockValue blocks, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, bool preview) + private RichTextBlockModel? ParseRichTextBlockModel(IPublishedElement owner, RichTextBlockValue blocks, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, bool preview) { RichTextConfiguration? configuration = propertyType.DataType.ConfigurationAs(); if (configuration?.Blocks?.Any() is not true) @@ -189,8 +207,8 @@ public class RteBlockRenderingValueConverter : SimpleTinyMceValueConverter, IDel return null; } - var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter, _jsonSerializer, _constructorCache); - return creator.CreateBlockModel(referenceCacheLevel, blocks, preview, configuration.Blocks); + var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter, _variationContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache); + return creator.CreateBlockModel(owner, referenceCacheLevel, blocks, preview, configuration.Blocks); } private string RenderRichTextBlockModel(string source, RichTextBlockModel? richTextBlockModel) @@ -200,10 +218,10 @@ public class RteBlockRenderingValueConverter : SimpleTinyMceValueConverter, IDel return source; } - var blocksByUdi = richTextBlockModel.ToDictionary(block => block.ContentUdi); + var blocksByKey = richTextBlockModel.ToDictionary(block => block.ContentKey); string RenderBlock(Match match) => - UdiParser.TryParse(match.Groups["udi"].Value, out Udi? udi) && blocksByUdi.TryGetValue(udi, out RichTextBlockItem? richTextBlockItem) + Guid.TryParse(match.Groups["key"].Value, out Guid key) && blocksByKey.TryGetValue(key, out RichTextBlockItem? richTextBlockItem) ? _partialViewBlockEngine.ExecuteAsync(richTextBlockItem).GetAwaiter().GetResult() : string.Empty; diff --git a/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs b/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs index 1e6f9e6898..6716dfae71 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs @@ -61,6 +61,9 @@ public class JsonBlockValueConverter : JsonConverter case nameof(BlockValue.Layout): DeserializeAndSetLayout(ref reader, options, typeToConvert, blockValue); break; + case nameof(BlockValue.Expose): + blockValue.Expose = DeserializeBlockVariation(ref reader, options, typeToConvert, nameof(BlockValue.Expose)); + break; } } } @@ -84,6 +87,9 @@ public class JsonBlockValueConverter : JsonConverter JsonSerializer.Serialize(writer, value.SettingsData, options); } + writer.WritePropertyName(nameof(BlockValue.Expose).ToFirstLowerInvariant()); + JsonSerializer.Serialize(writer, value.Expose, options); + Type layoutItemType = GetLayoutItemType(value.GetType()); writer.WriteStartObject(nameof(BlockValue.Layout)); @@ -115,7 +121,13 @@ public class JsonBlockValueConverter : JsonConverter } private List DeserializeBlockItemData(ref Utf8JsonReader reader, JsonSerializerOptions options, Type typeToConvert, string propertyName) - => JsonSerializer.Deserialize>(ref reader, options) + => DeserializeListOf(ref reader, options, typeToConvert, propertyName); + + private List DeserializeBlockVariation(ref Utf8JsonReader reader, JsonSerializerOptions options, Type typeToConvert, string propertyName) + => DeserializeListOf(ref reader, options, typeToConvert, propertyName); + + private List DeserializeListOf(ref Utf8JsonReader reader, JsonSerializerOptions options, Type typeToConvert, string propertyName) + => JsonSerializer.Deserialize>(ref reader, options) ?? throw new JsonException($"Unable to deserialize {propertyName} from type: {typeToConvert.FullName}."); private void DeserializeAndSetLayout(ref Utf8JsonReader reader, JsonSerializerOptions options, Type typeToConvert, BlockValue blockValue) @@ -167,12 +179,12 @@ public class JsonBlockValueConverter : JsonConverter } // did we encounter the concrete block value? - if (blockEditorAlias == blockValue.PropertyEditorAlias) + if (blockValue.SupportsBlockLayoutAlias(blockEditorAlias)) { // yes, deserialize the block layout items as their concrete type (list of layoutItemType) var layoutItems = JsonSerializer.Deserialize(ref reader, layoutItemsType, options); - blockValue.Layout[blockEditorAlias] = layoutItems as IEnumerable - ?? throw new JsonException($"Could not deserialize block editor layout items as type: {layoutItemType.FullName} while attempting to deserialize layout items for block editor alias: {blockEditorAlias} for type: {typeToConvert.FullName}."); + blockValue.Layout[blockValue.PropertyEditorAlias] = layoutItems as IEnumerable + ?? throw new JsonException($"Could not deserialize block editor layout items as type: {layoutItemType.FullName} while attempting to deserialize layout items for block editor alias: {blockEditorAlias} for type: {typeToConvert.FullName}."); } else { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index 8e4b836fee..67612b5756 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -1290,7 +1290,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent content.SetCultureName("name-fr", langFr.IsoCode); content.SetCultureName("name-da", langDa.IsoCode); - content.PublishCulture(CultureImpact.Explicit(langFr.IsoCode, langFr.IsDefault)); + content.PublishCulture(CultureImpact.Explicit(langFr.IsoCode, langFr.IsDefault), DateTime.Now, PropertyEditorCollection); var result = ContentService.CommitDocumentChanges(content); Assert.IsTrue(result.Success); content = ContentService.GetById(content.Id); @@ -1298,7 +1298,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent Assert.IsFalse(content.IsCulturePublished(langDa.IsoCode)); content.UnpublishCulture(langFr.IsoCode); - content.PublishCulture(CultureImpact.Explicit(langDa.IsoCode, langDa.IsDefault)); + content.PublishCulture(CultureImpact.Explicit(langDa.IsoCode, langDa.IsDefault), DateTime.Now, PropertyEditorCollection); result = ContentService.CommitDocumentChanges(content); Assert.IsTrue(result.Success); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index f4cee39978..e6ce8d467e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -60,6 +60,8 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest private FileSystems FileSystems => GetRequiredService(); + private PropertyEditorCollection PropertyEditorCollection => GetRequiredService(); + private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService(); @@ -224,7 +226,7 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest // publish = new edit version content1.SetValue("title", "title"); - content1.PublishCulture(CultureImpact.Invariant); + content1.PublishCulture(CultureImpact.Invariant, DateTime.Now, PropertyEditorCollection); content1.PublishedState = PublishedState.Publishing; repository.Save(content1); @@ -300,7 +302,7 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest new { id = content1.Id })); // publish = version - content1.PublishCulture(CultureImpact.Invariant); + content1.PublishCulture(CultureImpact.Invariant, DateTime.Now, PropertyEditorCollection); content1.PublishedState = PublishedState.Publishing; repository.Save(content1); @@ -344,7 +346,7 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest // publish = new version content1.Name = "name-4"; content1.SetValue("title", "title-4"); - content1.PublishCulture(CultureImpact.Invariant); + content1.PublishCulture(CultureImpact.Invariant, DateTime.Now, PropertyEditorCollection); content1.PublishedState = PublishedState.Publishing; repository.Save(content1); @@ -764,7 +766,7 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest // publish them all foreach (var content in result) { - content.PublishCulture(CultureImpact.Invariant); + content.PublishCulture(CultureImpact.Invariant, DateTime.Now, PropertyEditorCollection); repository.Save(content); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorBackwardsCompatibilityTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorBackwardsCompatibilityTests.cs new file mode 100644 index 0000000000..c4acaf7b01 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorBackwardsCompatibilityTests.cs @@ -0,0 +1,522 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class BlockEditorBackwardsCompatibilityTests : UmbracoIntegrationTest +{ + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + private IDataTypeService DataTypeService => GetRequiredService(); + + private PropertyEditorCollection PropertyEditorCollection => GetRequiredService(); + + private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService(); + + [TestCase] + public async Task BlockListIsBackwardsCompatible() + { + var elementType = await CreateElementType(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = await CreateContentType(blockListDataType); + + var json = $$""" + { + "layout": { + "{{Constants.PropertyEditors.Aliases.BlockList}}": [ + { + "contentUdi": "umb://element/1304e1ddac87439684fe8a399231cb3d", + "settingsUdi": "umb://element/1f613e26ce274898908a561437af5100" + }, + { + "contentUdi": "umb://element/0a4a416e547d464fabcc6f345c17809a", + "settingsUdi": "umb://element/63027539b0db45e7b70459762d4e83dd" + } + ] + }, + "contentData": [ + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/1304e1ddac87439684fe8a399231cb3d", + "title": "Content Title One", + "text": "Content Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/0a4a416e547d464fabcc6f345c17809a", + "title": "Content Title Two", + "text": "Content Text Two" + } + ], + "settingsData": [ + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/1f613e26ce274898908a561437af5100", + "title": "Settings Title One", + "text": "Settings Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/63027539b0db45e7b70459762d4e83dd", + "title": "Settings Title Two", + "text": "Settings Text Two" + } + ] + } + """; + + var contentBuilder = new ContentBuilder() + .WithContentType(contentType) + .WithName("Home"); + + var content = contentBuilder.Build(); + content.Properties["blocks"]!.SetValue(json); + ContentService.Save(content); + + var toEditor = blockListDataType.Editor!.GetValueEditor().ToEditor(content.Properties["blocks"]!) as BlockListValue; + Assert.IsNotNull(toEditor); + + Assert.Multiple(() => + { + Assert.AreEqual(2, toEditor.ContentData.Count); + + Assert.AreEqual("1304e1ddac87439684fe8a399231cb3d", toEditor.ContentData[0].Key.ToString("N")); + Assert.AreEqual("0a4a416e547d464fabcc6f345c17809a", toEditor.ContentData[1].Key.ToString("N")); + + AssertValueEquals(toEditor.ContentData[0], "title", "Content Title One"); + AssertValueEquals(toEditor.ContentData[0], "text", "Content Text One"); + AssertValueEquals(toEditor.ContentData[1], "title", "Content Title Two"); + AssertValueEquals(toEditor.ContentData[1], "text", "Content Text Two"); + + Assert.IsFalse(toEditor.ContentData[0].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.ContentData[1].RawPropertyValues.Any()); + }); + + Assert.Multiple(() => + { + Assert.AreEqual(2, toEditor.SettingsData.Count); + + Assert.AreEqual("1f613e26ce274898908a561437af5100", toEditor.SettingsData[0].Key.ToString("N")); + Assert.AreEqual("63027539b0db45e7b70459762d4e83dd", toEditor.SettingsData[1].Key.ToString("N")); + + AssertValueEquals(toEditor.SettingsData[0], "title", "Settings Title One"); + AssertValueEquals(toEditor.SettingsData[0], "text", "Settings Text One"); + AssertValueEquals(toEditor.SettingsData[1], "title", "Settings Title Two"); + AssertValueEquals(toEditor.SettingsData[1], "text", "Settings Text Two"); + + Assert.IsFalse(toEditor.SettingsData[0].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.SettingsData[1].RawPropertyValues.Any()); + }); + + Assert.Multiple(() => + { + Assert.AreEqual(2, toEditor.Expose.Count); + + Assert.AreEqual("1304e1ddac87439684fe8a399231cb3d", toEditor.Expose[0].ContentKey.ToString("N")); + Assert.AreEqual("0a4a416e547d464fabcc6f345c17809a", toEditor.Expose[1].ContentKey.ToString("N")); + }); + } + + [TestCase] + public async Task BlockGridIsBackwardsCompatible() + { + var elementType = await CreateElementType(); + var gridAreaKey = Guid.NewGuid(); + var blockGridDataType = await CreateBlockGridDataType(elementType, gridAreaKey); + var contentType = await CreateContentType(blockGridDataType); + + var json = $$""" + { + "layout": { + "{{Constants.PropertyEditors.Aliases.BlockGrid}}": [ + { + "contentUdi": "umb://element/1304e1ddac87439684fe8a399231cb3d", + "settingsUdi": "umb://element/1f613e26ce274898908a561437af5100", + "columnSpan": 12, + "rowSpan": 1, + "areas": [{ + "key": "{{gridAreaKey}}", + "items": [{ + "contentUdi": "umb://element/5fc866c590be4d01a28a979472a1ffee", + "areas": [], + "columnSpan": 12, + "rowSpan": 1 + }] + }] + }, + { + "contentUdi": "umb://element/0a4a416e547d464fabcc6f345c17809a", + "settingsUdi": "umb://element/63027539b0db45e7b70459762d4e83dd", + "columnSpan": 12, + "rowSpan": 1, + "areas": [{ + "key": "{{gridAreaKey}}", + "items": [{ + "contentUdi": "umb://element/264536b65b0f4641aa43d4bfb515831d", + "areas": [], + "columnSpan": 12, + "rowSpan": 1 + }] + }] + } + ] + }, + "contentData": [ + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/1304e1ddac87439684fe8a399231cb3d", + "title": "Content Title One", + "text": "Content Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/0a4a416e547d464fabcc6f345c17809a", + "title": "Content Title Two", + "text": "Content Text Two" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/5fc866c590be4d01a28a979472a1ffee", + "title": "Content Area Title One", + "text": "Content Area Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/264536b65b0f4641aa43d4bfb515831d", + "title": "Content Area Title Two", + "text": "Content Area Text Two" + } + ], + "settingsData": [ + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/1f613e26ce274898908a561437af5100", + "title": "Settings Title One", + "text": "Settings Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/63027539b0db45e7b70459762d4e83dd", + "title": "Settings Title Two", + "text": "Settings Text Two" + } + ] + } + """; + + var contentBuilder = new ContentBuilder() + .WithContentType(contentType) + .WithName("Home"); + + var content = contentBuilder.Build(); + content.Properties["blocks"]!.SetValue(json); + ContentService.Save(content); + + var toEditor = blockGridDataType.Editor!.GetValueEditor().ToEditor(content.Properties["blocks"]!) as BlockGridValue; + Assert.IsNotNull(toEditor); + + Assert.AreEqual(4, toEditor.ContentData.Count); + Assert.Multiple(() => + { + Assert.AreEqual("1304e1ddac87439684fe8a399231cb3d", toEditor.ContentData[0].Key.ToString("N")); + Assert.AreEqual("0a4a416e547d464fabcc6f345c17809a", toEditor.ContentData[1].Key.ToString("N")); + Assert.AreEqual("5fc866c590be4d01a28a979472a1ffee", toEditor.ContentData[2].Key.ToString("N")); + Assert.AreEqual("264536b65b0f4641aa43d4bfb515831d", toEditor.ContentData[3].Key.ToString("N")); + + AssertValueEquals(toEditor.ContentData[0], "title", "Content Title One"); + AssertValueEquals(toEditor.ContentData[0], "text", "Content Text One"); + AssertValueEquals(toEditor.ContentData[1], "title", "Content Title Two"); + AssertValueEquals(toEditor.ContentData[1], "text", "Content Text Two"); + AssertValueEquals(toEditor.ContentData[2], "title", "Content Area Title One"); + AssertValueEquals(toEditor.ContentData[2], "text", "Content Area Text One"); + AssertValueEquals(toEditor.ContentData[3], "title", "Content Area Title Two"); + AssertValueEquals(toEditor.ContentData[3], "text", "Content Area Text Two"); + + Assert.IsFalse(toEditor.ContentData[0].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.ContentData[1].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.ContentData[2].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.ContentData[3].RawPropertyValues.Any()); + }); + + Assert.AreEqual(2, toEditor.SettingsData.Count); + Assert.Multiple(() => + { + Assert.AreEqual("1f613e26ce274898908a561437af5100", toEditor.SettingsData[0].Key.ToString("N")); + Assert.AreEqual("63027539b0db45e7b70459762d4e83dd", toEditor.SettingsData[1].Key.ToString("N")); + + AssertValueEquals(toEditor.SettingsData[0], "title", "Settings Title One"); + AssertValueEquals(toEditor.SettingsData[0], "text", "Settings Text One"); + AssertValueEquals(toEditor.SettingsData[1], "title", "Settings Title Two"); + AssertValueEquals(toEditor.SettingsData[1], "text", "Settings Text Two"); + + Assert.IsFalse(toEditor.SettingsData[0].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.SettingsData[1].RawPropertyValues.Any()); + }); + + Assert.Multiple(() => + { + Assert.AreEqual(4, toEditor.Expose.Count); + + Assert.AreEqual("1304e1ddac87439684fe8a399231cb3d", toEditor.Expose[0].ContentKey.ToString("N")); + Assert.AreEqual("0a4a416e547d464fabcc6f345c17809a", toEditor.Expose[1].ContentKey.ToString("N")); + Assert.AreEqual("5fc866c590be4d01a28a979472a1ffee", toEditor.Expose[2].ContentKey.ToString("N")); + Assert.AreEqual("264536b65b0f4641aa43d4bfb515831d", toEditor.Expose[3].ContentKey.ToString("N")); + }); + } + + [TestCase] + public async Task RichTextIsBackwardsCompatible() + { + var elementType = await CreateElementType(); + var richTextDataType = await CreateRichTextDataType(elementType); + var contentType = await CreateContentType(richTextDataType); + + var json = $$""" + { + "markup": "

huh?

", + "blocks": { + "layout": { + "{{Constants.PropertyEditors.Aliases.TinyMce}}": [ + { + "contentUdi": "umb://element/1304e1ddac87439684fe8a399231cb3d", + "settingsUdi": "umb://element/1f613e26ce274898908a561437af5100" + }, + { + "contentUdi": "umb://element/0a4a416e547d464fabcc6f345c17809a", + "settingsUdi": "umb://element/63027539b0db45e7b70459762d4e83dd" + } + ] + }, + "contentData": [ + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/1304e1ddac87439684fe8a399231cb3d", + "title": "Content Title One", + "text": "Content Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/0a4a416e547d464fabcc6f345c17809a", + "title": "Content Title Two", + "text": "Content Text Two" + } + ], + "settingsData": [ + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/1f613e26ce274898908a561437af5100", + "title": "Settings Title One", + "text": "Settings Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/63027539b0db45e7b70459762d4e83dd", + "title": "Settings Title Two", + "text": "Settings Text Two" + } + ] + } + } + """; + + var contentBuilder = new ContentBuilder() + .WithContentType(contentType) + .WithName("Home"); + + var content = contentBuilder.Build(); + content.Properties["blocks"]!.SetValue(json); + ContentService.Save(content); + + var toEditor = richTextDataType.Editor!.GetValueEditor().ToEditor(content.Properties["blocks"]!) as RichTextEditorValue; + Assert.IsNotNull(toEditor); + Assert.IsNotNull(toEditor.Blocks); + + Assert.Multiple(() => + { + Assert.AreEqual(2, toEditor.Blocks.ContentData.Count); + + Assert.AreEqual("1304e1ddac87439684fe8a399231cb3d", toEditor.Blocks.ContentData[0].Key.ToString("N")); + Assert.AreEqual("0a4a416e547d464fabcc6f345c17809a", toEditor.Blocks.ContentData[1].Key.ToString("N")); + + AssertValueEquals(toEditor.Blocks.ContentData[0], "title", "Content Title One"); + AssertValueEquals(toEditor.Blocks.ContentData[0], "text", "Content Text One"); + AssertValueEquals(toEditor.Blocks.ContentData[1], "title", "Content Title Two"); + AssertValueEquals(toEditor.Blocks.ContentData[1], "text", "Content Text Two"); + + Assert.IsFalse(toEditor.Blocks.ContentData[0].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.Blocks.ContentData[1].RawPropertyValues.Any()); + }); + + Assert.Multiple(() => + { + Assert.AreEqual(2, toEditor.Blocks.SettingsData.Count); + + Assert.AreEqual("1f613e26ce274898908a561437af5100", toEditor.Blocks.SettingsData[0].Key.ToString("N")); + Assert.AreEqual("63027539b0db45e7b70459762d4e83dd", toEditor.Blocks.SettingsData[1].Key.ToString("N")); + + AssertValueEquals(toEditor.Blocks.SettingsData[0], "title", "Settings Title One"); + AssertValueEquals(toEditor.Blocks.SettingsData[0], "text", "Settings Text One"); + AssertValueEquals(toEditor.Blocks.SettingsData[1], "title", "Settings Title Two"); + AssertValueEquals(toEditor.Blocks.SettingsData[1], "text", "Settings Text Two"); + + Assert.IsFalse(toEditor.Blocks.SettingsData[0].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.Blocks.SettingsData[1].RawPropertyValues.Any()); + }); + + Assert.Multiple(() => + { + Assert.AreEqual(2, toEditor.Blocks.Expose.Count); + + Assert.AreEqual("1304e1ddac87439684fe8a399231cb3d", toEditor.Blocks.Expose[0].ContentKey.ToString("N")); + Assert.AreEqual("0a4a416e547d464fabcc6f345c17809a", toEditor.Blocks.Expose[1].ContentKey.ToString("N")); + }); + } + + private static void AssertValueEquals(BlockItemData blockItemData, string propertyAlias, string expectedValue) + { + var blockPropertyValue = blockItemData.Values.FirstOrDefault(v => v.Alias == propertyAlias); + Assert.IsNotNull(blockPropertyValue); + Assert.AreEqual(expectedValue, blockPropertyValue.Value); + } + + private async Task CreateElementType() + { + var elementType = new ContentTypeBuilder() + .WithAlias("myElementType") + .WithName("My Element Type") + .WithIsElement(true) + .AddPropertyType() + .WithAlias("title") + .WithName("Title") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .Done() + .AddPropertyType() + .WithAlias("text") + .WithName("Text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .Done() + .Build(); + + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + return elementType; + } + + private async Task CreateBlockListDataType(IContentType elementType) + { + var dataType = new DataType(PropertyEditorCollection[Constants.PropertyEditors.Aliases.BlockList], ConfigurationEditorJsonSerializer) + { + ConfigurationData = new Dictionary + { + { + "blocks", + new BlockListConfiguration.BlockConfiguration[] + { + new() { ContentElementTypeKey = elementType.Key, SettingsElementTypeKey = elementType.Key } + } + } + }, + Name = "My Block List", + DatabaseType = ValueStorageType.Ntext, + ParentId = Constants.System.Root, + CreateDate = DateTime.UtcNow + }; + + await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + return dataType; + } + + private async Task CreateBlockGridDataType(IContentType elementType, Guid gridAreaKey) + { + var dataType = new DataType(PropertyEditorCollection[Constants.PropertyEditors.Aliases.BlockGrid], ConfigurationEditorJsonSerializer) + { + ConfigurationData = new Dictionary + { + { + "blocks", + new BlockGridConfiguration.BlockGridBlockConfiguration[] + { + new() + { + ContentElementTypeKey = elementType.Key, + SettingsElementTypeKey = elementType.Key, + AllowInAreas = true, + AllowAtRoot = true, + Areas = + [ + new BlockGridConfiguration.BlockGridAreaConfiguration + { + Key = gridAreaKey, + Alias = "areaOne" + } + ] + } + } + } + }, + Name = "My Block Grid", + DatabaseType = ValueStorageType.Ntext, + ParentId = Constants.System.Root, + CreateDate = DateTime.UtcNow + }; + + await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + return dataType; + } + + private async Task CreateRichTextDataType(IContentType elementType) + { + var dataType = new DataType(PropertyEditorCollection[Constants.PropertyEditors.Aliases.RichText], ConfigurationEditorJsonSerializer) + { + ConfigurationData = new Dictionary + { + { + "blocks", + new RichTextConfiguration.RichTextBlockConfiguration[] + { + new() { ContentElementTypeKey = elementType.Key, SettingsElementTypeKey = elementType.Key } + } + } + }, + Name = "My Rich Text", + DatabaseType = ValueStorageType.Ntext, + ParentId = Constants.System.Root, + CreateDate = DateTime.UtcNow + }; + + await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + return dataType; + } + + private async Task CreateContentType(IDataType blockEditorDataType) + { + var contentType = new ContentTypeBuilder() + .WithAlias("myPage") + .WithName("My Page") + .AddPropertyType() + .WithAlias("blocks") + .WithName("Blocks") + .WithDataTypeId(blockEditorDataType.Id) + .Done() + .Build(); + + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + return contentType; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs new file mode 100644 index 0000000000..2c1f69de8a --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs @@ -0,0 +1,172 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Tests.Common; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public abstract class BlockEditorElementVariationTestBase : UmbracoIntegrationTest +{ + protected List TestsRequiringAllowEditInvariantFromNonDefault { get; set; } = new(); + + protected ILanguageService LanguageService => GetRequiredService(); + + protected IContentService ContentService => GetRequiredService(); + + protected IContentTypeService ContentTypeService => GetRequiredService(); + + protected PropertyEditorCollection PropertyEditorCollection => GetRequiredService(); + + private IPublishedSnapshotService PublishedSnapshotService => GetRequiredService(); + + private IUmbracoContextAccessor UmbracoContextAccessor => GetRequiredService(); + + private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService(); + + private IVariationContextAccessor VariationContextAccessor => GetRequiredService(); + + private IDataTypeService DataTypeService => GetRequiredService(); + + private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + var mockHttpContextAccessor = new Mock(); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost"); + + mockHttpContextAccessor.SetupGet(x => x.HttpContext).Returns(httpContext); + + builder.Services.AddUnique(); + builder.Services.AddUnique(mockHttpContextAccessor.Object); + builder.AddUmbracoHybridCache(); + builder.AddNuCache(); + + builder.Services.Configure(config => + config.AllowEditInvariantFromNonDefault = TestsRequiringAllowEditInvariantFromNonDefault.Contains(TestContext.CurrentContext.Test.Name)); + } + + [SetUp] + public async Task SetUp() => await LanguageService.CreateAsync( + new Language("da-DK", "Danish"), Constants.Security.SuperUserKey); + + protected void PublishContent(IContent content, string[] culturesToPublish) + { + var publishResult = ContentService.Publish(content, culturesToPublish); + Assert.IsTrue(publishResult.Success); + + ContentCacheRefresher.JsonPayload[] payloads = + [ + new ContentCacheRefresher.JsonPayload + { + ChangeTypes = TreeChangeTypes.RefreshNode, + Key = content.Key, + Id = content.Id, + Blueprint = false + } + ]; + + PublishedSnapshotService.Notify(payloads, out _, out _); + } + + protected IContentType CreateElementType(ContentVariation variation, string alias = "myElementType") + { + var elementType = new ContentTypeBuilder() + .WithAlias(alias) + .WithName("My Element Type") + .WithIsElement(true) + .WithContentVariation(variation) + .AddPropertyType() + .WithAlias("invariantText") + .WithName("Invariant text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Nothing) + .Done() + .AddPropertyType() + .WithAlias("variantText") + .WithName("Variant text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(variation) + .Done() + .Build(); + ContentTypeService.Save(elementType); + return elementType; + } + + protected IContentType CreateContentType(ContentVariation contentTypeVariation, IDataType blocksEditorDataType, ContentVariation blocksPropertyVariation = ContentVariation.Nothing) + { + var contentType = new ContentTypeBuilder() + .WithAlias("myPage") + .WithName("My Page") + .WithContentVariation(contentTypeVariation) + .AddPropertyType() + .WithAlias("blocks") + .WithName("Blocks") + .WithDataTypeId(blocksEditorDataType.Id) + .WithVariations(blocksPropertyVariation) + .Done() + .Build(); + ContentTypeService.Save(contentType); + return contentType; + } + + protected IPublishedContent GetPublishedContent(Guid key) + { + UmbracoContextAccessor.Clear(); + var umbracoContext = UmbracoContextFactory.EnsureUmbracoContext().UmbracoContext; + var publishedContent = umbracoContext.Content?.GetById(key); + Assert.IsNotNull(publishedContent); + + return publishedContent; + } + + protected void SetVariationContext(string? culture, string? segment) + => VariationContextAccessor.VariationContext = new VariationContext(culture: culture, segment: segment); + + protected async Task CreateBlockEditorDataType(string propertyEditorAlias, T blocksConfiguration) + { + var dataType = new DataType(PropertyEditorCollection[propertyEditorAlias], ConfigurationEditorJsonSerializer) + { + ConfigurationData = new Dictionary { { "blocks", blocksConfiguration } }, + Name = "My Block Editor", + DatabaseType = ValueStorageType.Ntext, + ParentId = Constants.System.Root, + CreateDate = DateTime.UtcNow + }; + + await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + return dataType; + } + + protected void RefreshContentTypeCache(params IContentType[] contentTypes) + { + ContentTypeCacheRefresher.JsonPayload[] payloads = contentTypes + .Select(contentType => new ContentTypeCacheRefresher.JsonPayload(nameof(IContentType), contentType.Id, ContentTypeChangeTypes.RefreshMain)) + .ToArray(); + + PublishedSnapshotService.Notify(payloads); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockGridElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockGridElementLevelVariationTests.cs new file mode 100644 index 0000000000..0122eaee67 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockGridElementLevelVariationTests.cs @@ -0,0 +1,533 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Tests.Common.Builders; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +// NOTE: These tests are in place to ensure that element level variation works for Block Grid. Element level variation +// is tested more in-depth for Block List (see BlockListElementLevelVariationTests), but since the actual +// implementation is shared between Block List and Block Grid, we won't repeat all those tests here. +public class BlockGridElementLevelVariationTests : BlockEditorElementVariationTestBase +{ + private IJsonSerializer JsonSerializer => GetRequiredService(); + + [Test] + public async Task Can_Publish_Cultures_Independently() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var areaKey = Guid.NewGuid(); + var blockGridDataType = await CreateBlockGridDataType(elementType, areaKey); + var contentType = CreateContentType(blockGridDataType); + var blockGridValue = CreateBlockGridValue(elementType, areaKey); + var content = CreateContent(contentType, blockGridValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValues( + "en-US", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant content value", element1.Value("invariantText")); + Assert.AreEqual("#1: The first content value in English", element1.Value("variantText")); + Assert.AreEqual("#2: The first invariant content value", element2.Value("invariantText")); + Assert.AreEqual("#2: The first content value in English", element2.Value("variantText")); + Assert.AreEqual("#3: The first invariant content value", element3.Value("invariantText")); + Assert.AreEqual("#3: The first content value in English", element3.Value("variantText")); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant settings value", element1.Value("invariantText")); + Assert.AreEqual("#1: The first settings value in English", element1.Value("variantText")); + Assert.AreEqual("#2: The first invariant settings value", element2.Value("invariantText")); + Assert.AreEqual("#2: The first settings value in English", element2.Value("variantText")); + Assert.AreEqual("#3: The first invariant settings value", element3.Value("invariantText")); + Assert.AreEqual("#3: The first settings value in English", element3.Value("variantText")); + }); + + AssertPropertyValues( + "da-DK", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant content value", element1.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", element1.Value("variantText")); + Assert.AreEqual("#2: The first invariant content value", element2.Value("invariantText")); + Assert.AreEqual("#2: The first content value in Danish", element2.Value("variantText")); + Assert.AreEqual("#3: The first invariant content value", element3.Value("invariantText")); + Assert.AreEqual("#3: The first content value in Danish", element3.Value("variantText")); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant settings value", element1.Value("invariantText")); + Assert.AreEqual("#1: The first settings value in Danish", element1.Value("variantText")); + Assert.AreEqual("#2: The first invariant settings value", element2.Value("invariantText")); + Assert.AreEqual("#2: The first settings value in Danish", element2.Value("variantText")); + Assert.AreEqual("#3: The first invariant settings value", element3.Value("invariantText")); + Assert.AreEqual("#3: The first settings value in Danish", element3.Value("variantText")); + }); + + blockGridValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + for (var i = 0; i < 3; i++) + { + blockGridValue.ContentData[i].Values[0].Value = $"#{i + 1}: The second invariant content value"; + blockGridValue.ContentData[i].Values[1].Value = $"#{i + 1}: The second content value in English"; + blockGridValue.ContentData[i].Values[2].Value = $"#{i + 1}: The second content value in Danish"; + blockGridValue.SettingsData[i].Values[0].Value = $"#{i + 1}: The second invariant settings value"; + blockGridValue.SettingsData[i].Values[1].Value = $"#{i + 1}: The second settings value in English"; + blockGridValue.SettingsData[i].Values[2].Value = $"#{i + 1}: The second settings value in Danish"; + } + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockGridValue)); + ContentService.Save(content); + PublishContent(content, ["en-US"]); + + AssertPropertyValues( + "en-US", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant content value", element1.Value("invariantText")); + Assert.AreEqual("#1: The second content value in English", element1.Value("variantText")); + Assert.AreEqual("#2: The second invariant content value", element2.Value("invariantText")); + Assert.AreEqual("#2: The second content value in English", element2.Value("variantText")); + Assert.AreEqual("#3: The second invariant content value", element3.Value("invariantText")); + Assert.AreEqual("#3: The second content value in English", element3.Value("variantText")); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant settings value", element1.Value("invariantText")); + Assert.AreEqual("#1: The second settings value in English", element1.Value("variantText")); + Assert.AreEqual("#2: The second invariant settings value", element2.Value("invariantText")); + Assert.AreEqual("#2: The second settings value in English", element2.Value("variantText")); + Assert.AreEqual("#3: The second invariant settings value", element3.Value("invariantText")); + Assert.AreEqual("#3: The second settings value in English", element3.Value("variantText")); + }); + + AssertPropertyValues( + "da-DK", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant content value", element1.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", element1.Value("variantText")); + Assert.AreEqual("#2: The second invariant content value", element2.Value("invariantText")); + Assert.AreEqual("#2: The first content value in Danish", element2.Value("variantText")); + Assert.AreEqual("#3: The second invariant content value", element3.Value("invariantText")); + Assert.AreEqual("#3: The first content value in Danish", element3.Value("variantText")); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant settings value", element1.Value("invariantText")); + Assert.AreEqual("#1: The first settings value in Danish", element1.Value("variantText")); + Assert.AreEqual("#2: The second invariant settings value", element2.Value("invariantText")); + Assert.AreEqual("#2: The first settings value in Danish", element2.Value("variantText")); + Assert.AreEqual("#3: The second invariant settings value", element3.Value("invariantText")); + Assert.AreEqual("#3: The first settings value in Danish", element3.Value("variantText")); + }); + + PublishContent(content, ["da-DK"]); + + AssertPropertyValues( + "da-DK", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant content value", element1.Value("invariantText")); + Assert.AreEqual("#1: The second content value in Danish", element1.Value("variantText")); + Assert.AreEqual("#2: The second invariant content value", element2.Value("invariantText")); + Assert.AreEqual("#2: The second content value in Danish", element2.Value("variantText")); + Assert.AreEqual("#3: The second invariant content value", element3.Value("invariantText")); + Assert.AreEqual("#3: The second content value in Danish", element3.Value("variantText")); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant settings value", element1.Value("invariantText")); + Assert.AreEqual("#1: The second settings value in Danish", element1.Value("variantText")); + Assert.AreEqual("#2: The second invariant settings value", element2.Value("invariantText")); + Assert.AreEqual("#2: The second settings value in Danish", element2.Value("variantText")); + Assert.AreEqual("#3: The second invariant settings value", element3.Value("invariantText")); + Assert.AreEqual("#3: The second settings value in Danish", element3.Value("variantText")); + }); + + void AssertPropertyValues( + string culture, + Action validateBlockContentValues, + Action validateBlockSettingsValues) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var blocks = publishedContent.Value("blocks"); + Assert.IsNotNull(blocks); + Assert.AreEqual(2, blocks.Count); + var area = blocks[0].Areas.FirstOrDefault(); + Assert.IsNotNull(area); + Assert.AreEqual(1, area.Count); + Assert.Multiple(() => + { + validateBlockContentValues(blocks[0].Content, area[0].Content, blocks[1].Content); + validateBlockSettingsValues(blocks[0].Settings, area[0].Settings, blocks[1].Settings); + }); + } + } + + [Test] + public async Task Can_Publish_With_Blocks_Removed_At_Root() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var areaKey = Guid.NewGuid(); + var blockGridDataType = await CreateBlockGridDataType(elementType, areaKey); + var contentType = CreateContentType(blockGridDataType); + var blockGridValue = CreateBlockGridValue(elementType, areaKey); + var content = CreateContent(contentType, blockGridValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 2, blocks => { }); + AssertPropertyValues("da-DK", 2, blocks => { }); + + blockGridValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + + // remove block #3 (second at root level) + blockGridValue.Layout[blockGridValue.Layout.First().Key] = + [ + blockGridValue.Layout.First().Value.First(), + ]; + var contentKey = blockGridValue.ContentData[2].Key; + blockGridValue.ContentData.RemoveAt(2); + blockGridValue.SettingsData.RemoveAt(2); + blockGridValue.Expose.RemoveAll(v => v.ContentKey == contentKey); + Assert.AreEqual(4, blockGridValue.Expose.Count); + + for (var i = 0; i < 2; i++) + { + blockGridValue.ContentData[i].Values[0].Value = $"#{i + 1}: The second invariant content value"; + blockGridValue.ContentData[i].Values[1].Value = $"#{i + 1}: The second content value in English"; + blockGridValue.ContentData[i].Values[2].Value = $"#{i + 1}: The second content value in Danish"; + blockGridValue.SettingsData[i].Values[0].Value = $"#{i + 1}: The second invariant settings value"; + blockGridValue.SettingsData[i].Values[1].Value = $"#{i + 1}: The second settings value in English"; + blockGridValue.SettingsData[i].Values[2].Value = $"#{i + 1}: The second settings value in Danish"; + } + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockGridValue)); + ContentService.Save(content); + PublishContent(content, ["en-US"]); + + AssertPropertyValues("en-US", 1, blocks => + { + var areaItem = blocks[0].Areas.FirstOrDefault()?.FirstOrDefault(); + Assert.IsNotNull(areaItem); + + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in English", blocks[0].Content.Value("variantText")); + Assert.AreEqual("#2: The second invariant content value", areaItem.Content.Value("invariantText")); + Assert.AreEqual("#2: The second content value in English", areaItem.Content.Value("variantText")); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Value("invariantText")); + Assert.AreEqual("#1: The second settings value in English", blocks[0].Settings.Value("variantText")); + Assert.AreEqual("#2: The second invariant settings value", areaItem.Settings!.Value("invariantText")); + Assert.AreEqual("#2: The second settings value in English", areaItem.Settings.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 1, blocks => + { + var areaItem = blocks[0].Areas.FirstOrDefault()?.FirstOrDefault(); + Assert.IsNotNull(areaItem); + + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Value("variantText")); + Assert.AreEqual("#2: The second invariant content value", areaItem.Content.Value("invariantText")); + Assert.AreEqual("#2: The first content value in Danish", areaItem.Content.Value("variantText")); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Value("invariantText")); + Assert.AreEqual("#1: The first settings value in Danish", blocks[0].Settings.Value("variantText")); + Assert.AreEqual("#2: The second invariant settings value", areaItem.Settings!.Value("invariantText")); + Assert.AreEqual("#2: The first settings value in Danish", areaItem.Settings.Value("variantText")); + }); + + PublishContent(content, ["da-DK"]); + + AssertPropertyValues("da-DK", 1, blocks => + { + var areaItem = blocks[0].Areas.FirstOrDefault()?.FirstOrDefault(); + Assert.IsNotNull(areaItem); + + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in Danish", blocks[0].Content.Value("variantText")); + Assert.AreEqual("#2: The second invariant content value", areaItem.Content.Value("invariantText")); + Assert.AreEqual("#2: The second content value in Danish", areaItem.Content.Value("variantText")); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Value("invariantText")); + Assert.AreEqual("#1: The second settings value in Danish", blocks[0].Settings.Value("variantText")); + Assert.AreEqual("#2: The second invariant settings value", areaItem.Settings!.Value("invariantText")); + Assert.AreEqual("#2: The second settings value in Danish", areaItem.Settings.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + [Test] + public async Task Can_Publish_With_Blocks_Removed_In_Area() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var areaKey = Guid.NewGuid(); + var blockGridDataType = await CreateBlockGridDataType(elementType, areaKey); + var contentType = CreateContentType(blockGridDataType); + var blockGridValue = CreateBlockGridValue(elementType, areaKey); + var content = CreateContent(contentType, blockGridValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.IsNotEmpty(blocks[0].Areas); + + // no need to validate the content/settings values here, the same thing is validated in another test + }); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.IsNotEmpty(blocks[0].Areas); + + // no need to validate the content/settings values here, the same thing is validated in another test + }); + + blockGridValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + + // remove block #2 (inside the area of the first block at root level) + ((BlockGridLayoutItem)blockGridValue.Layout[blockGridValue.Layout.First().Key].First()).Areas = []; + var contentKey = blockGridValue.ContentData[1].Key; + blockGridValue.ContentData.RemoveAt(1); + blockGridValue.SettingsData.RemoveAt(1); + blockGridValue.Expose.RemoveAll(v => v.ContentKey == contentKey); + Assert.AreEqual(4, blockGridValue.Expose.Count); + + blockGridValue.ContentData[0].Values[0].Value = "#1: The second invariant content value"; + blockGridValue.ContentData[0].Values[1].Value = "#1: The second content value in English"; + blockGridValue.ContentData[0].Values[2].Value = "#1: The second content value in Danish"; + blockGridValue.ContentData[1].Values[0].Value = "#3: The second invariant content value"; + blockGridValue.ContentData[1].Values[1].Value = "#3: The second content value in English"; + blockGridValue.ContentData[1].Values[2].Value = "#3: The second content value in Danish"; + blockGridValue.SettingsData[0].Values[0].Value = "#1: The second invariant settings value"; + blockGridValue.SettingsData[0].Values[1].Value = "#1: The second settings value in English"; + blockGridValue.SettingsData[0].Values[2].Value = "#1: The second settings value in Danish"; + blockGridValue.SettingsData[1].Values[0].Value = "#3: The second invariant settings value"; + blockGridValue.SettingsData[1].Values[1].Value = "#3: The second settings value in English"; + blockGridValue.SettingsData[1].Values[2].Value = "#3: The second settings value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockGridValue)); + ContentService.Save(content); + PublishContent(content, ["en-US"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.IsEmpty(blocks[0].Areas); + + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in English", blocks[0].Content.Value("variantText")); + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#3: The second content value in English", blocks[1].Content.Value("variantText")); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Value("invariantText")); + Assert.AreEqual("#1: The second settings value in English", blocks[0].Settings.Value("variantText")); + Assert.AreEqual("#3: The second invariant settings value", blocks[1].Settings!.Value("invariantText")); + Assert.AreEqual("#3: The second settings value in English", blocks[1].Settings.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.IsEmpty(blocks[0].Areas); + + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Value("variantText")); + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#3: The first content value in Danish", blocks[1].Content.Value("variantText")); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Value("invariantText")); + Assert.AreEqual("#1: The first settings value in Danish", blocks[0].Settings.Value("variantText")); + Assert.AreEqual("#3: The second invariant settings value", blocks[1].Settings!.Value("invariantText")); + Assert.AreEqual("#3: The first settings value in Danish", blocks[1].Settings.Value("variantText")); + }); + + PublishContent(content, ["da-DK"]); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.IsEmpty(blocks[0].Areas); + + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in Danish", blocks[0].Content.Value("variantText")); + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#3: The second content value in Danish", blocks[1].Content.Value("variantText")); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Value("invariantText")); + Assert.AreEqual("#1: The second settings value in Danish", blocks[0].Settings.Value("variantText")); + Assert.AreEqual("#3: The second invariant settings value", blocks[1].Settings!.Value("invariantText")); + Assert.AreEqual("#3: The second settings value in Danish", blocks[1].Settings.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + private async Task CreateBlockGridDataType(IContentType elementType, Guid areaKey) + => await CreateBlockEditorDataType( + Constants.PropertyEditors.Aliases.BlockGrid, + new BlockGridConfiguration.BlockGridBlockConfiguration[] + { + new() + { + ContentElementTypeKey = elementType.Key, + SettingsElementTypeKey = elementType.Key, + AreaGridColumns = 12, + Areas = + [ + new() { Alias = "one", Key = areaKey, ColumnSpan = 12, RowSpan = 1 } + ] + } + }); + + private IContentType CreateContentType(IDataType blockListDataType) + => CreateContentType(ContentVariation.Culture, blockListDataType); + + private BlockGridValue CreateBlockGridValue(IContentType elementType, Guid areaKey) + { + var contentElementKey1 = Guid.NewGuid(); + var settingsElementKey1 = Guid.NewGuid(); + var contentElementKey2 = Guid.NewGuid(); + var settingsElementKey2 = Guid.NewGuid(); + var contentElementKey3 = Guid.NewGuid(); + var settingsElementKey3 = Guid.NewGuid(); + return new BlockGridValue( + [ + new BlockGridLayoutItem(contentElementKey1, settingsElementKey1) + { + ColumnSpan = 12, + RowSpan = 1, + Areas = + [ + new BlockGridLayoutAreaItem(areaKey) + { + Items = + [ + new BlockGridLayoutItem(contentElementKey2, settingsElementKey2) + { + ColumnSpan = 12, + RowSpan = 1 + }, + ], + }, + ], + }, + new BlockGridLayoutItem(contentElementKey3, settingsElementKey3) + { + ColumnSpan = 12, + RowSpan = 1, + } + ]) + { + ContentData = + [ + new(contentElementKey1, elementType.Key, elementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#1: The first invariant content value" }, + new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" } + ] + }, + new(contentElementKey2, elementType.Key, elementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#2: The first invariant content value" }, + new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" } + ] + }, + new(contentElementKey3, elementType.Key, elementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#3: The first invariant content value" }, + new() { Alias = "variantText", Value = "#3: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#3: The first content value in Danish", Culture = "da-DK" } + ] + }, + ], + SettingsData = + [ + new(settingsElementKey1, elementType.Key, elementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#1: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" } + ] + }, + new(settingsElementKey2, elementType.Key, elementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#2: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" } + ] + }, + new(settingsElementKey3, elementType.Key, elementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#3: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#3: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#3: The first settings value in Danish", Culture = "da-DK" } + ] + }, + ], + Expose = + [ + new (contentElementKey1, "en-US", null), + new (contentElementKey1, "da-DK", null), + new (contentElementKey2, "en-US", null), + new (contentElementKey2, "da-DK", null), + new (contentElementKey3, "en-US", null), + new (contentElementKey3, "da-DK", null), + ] + }; + } + + private IContent CreateContent(IContentType contentType, BlockGridValue blockGridValue) + { + var contentBuilder = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName("en-US", "Home (en)") + .WithCultureName("da-DK", "Home (da)"); + + var content = contentBuilder.Build(); + + var propertyValue = JsonSerializer.Serialize(blockGridValue); + content.Properties["blocks"]!.SetValue(propertyValue); + + ContentService.Save(content); + return content; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Parsing.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Parsing.cs new file mode 100644 index 0000000000..7433417df7 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Parsing.cs @@ -0,0 +1,662 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +public partial class BlockListElementLevelVariationTests +{ + [TestCase("en-US", "The culture variant content value in English", "The culture variant settings value in English")] + [TestCase("da-DK", "The culture variant content value in Danish", "The culture variant settings value in Danish")] + public async Task Can_Parse_Element_Level_Culture_Variations(string culture, string expectedVariantContentValue, string expectedVariantSettingsValue) + { + var publishedContent = await CreatePublishedContent( + ContentVariation.Culture, + new List + { + new() { Alias = "invariantText", Value = "The invariant content value" }, + new() { Alias = "variantText", Value = "The culture variant content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The culture variant content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The invariant settings value" }, + new() { Alias = "variantText", Value = "The culture variant settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The culture variant settings value in Danish", Culture = "da-DK" }, + }); + + SetVariationContext(culture, null); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant content value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual(expectedVariantContentValue, variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant settings value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual(expectedVariantSettingsValue, variantProperty.GetValue()); + }); + } + + [TestCase("segment1", "The segment variant content value for Segment1", "The segment variant settings value for Segment1")] + [TestCase("segment2", "The segment variant content value for Segment2", "The segment variant settings value for Segment2")] + public async Task Can_Parse_Element_Level_Segment_Variations(string segment, string expectedVariantContentValue, string expectedVariantSettingsValue) + { + var publishedContent = await CreatePublishedContent( + ContentVariation.Segment, + new List + { + new() { Alias = "invariantText", Value = "The invariant content value" }, + new() { Alias = "variantText", Value = "The segment variant content value for Segment1", Segment = "segment1" }, + new() { Alias = "variantText", Value = "The segment variant content value for Segment2", Segment = "segment2" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The invariant settings value" }, + new() { Alias = "variantText", Value = "The segment variant settings value for Segment1", Segment = "segment1" }, + new() { Alias = "variantText", Value = "The segment variant settings value for Segment2", Segment = "segment2" }, + }); + + SetVariationContext(null, segment); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant content value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual(expectedVariantContentValue, variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant settings value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual(expectedVariantSettingsValue, variantProperty.GetValue()); + }); + } + + [TestCase( + "en-US", + "segment1", + "The variant content value in English for Segment1", + "The variant settings value in English for Segment1")] + [TestCase( + "en-US", + "segment2", + "The variant content value in English for Segment2", + "The variant settings value in English for Segment2")] + [TestCase( + "da-DK", + "segment1", + "The variant content value in Danish for Segment1", + "The variant settings value in Danish for Segment1")] + [TestCase( + "da-DK", + "segment2", + "The variant content value in Danish for Segment2", + "The variant settings value in Danish for Segment2")] + public async Task Can_Parse_Element_Level_Culture_And_Segment_Variations( + string culture, + string segment, + string expectedVariantContentValue, + string expectedVariantSettingsValue) + { + var publishedContent = await CreatePublishedContent( + ContentVariation.CultureAndSegment, + new List + { + new() { Alias = "invariantText", Value = "The invariant content value" }, + new() { Alias = "variantText", Value = "The variant content value in English for Segment1", Culture = "en-US", Segment = "segment1" }, + new() { Alias = "variantText", Value = "The variant content value in English for Segment2", Culture = "en-US", Segment = "segment2" }, + new() { Alias = "variantText", Value = "The variant content value in Danish for Segment1", Culture = "da-DK", Segment = "segment1" }, + new() { Alias = "variantText", Value = "The variant content value in Danish for Segment2", Culture = "da-DK", Segment = "segment2" } + }, + new List + { + new() { Alias = "invariantText", Value = "The invariant settings value" }, + new() { Alias = "variantText", Value = "The variant settings value in English for Segment1", Culture = "en-US", Segment = "segment1" }, + new() { Alias = "variantText", Value = "The variant settings value in English for Segment2", Culture = "en-US", Segment = "segment2" }, + new() { Alias = "variantText", Value = "The variant settings value in Danish for Segment1", Culture = "da-DK", Segment = "segment1" }, + new() { Alias = "variantText", Value = "The variant settings value in Danish for Segment2", Culture = "da-DK", Segment = "segment2" } + }); + + SetVariationContext(culture, segment); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant content value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual(expectedVariantContentValue, variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant settings value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual(expectedVariantSettingsValue, variantProperty.GetValue()); + }); + } + + [TestCase("en-US")] + [TestCase("da-DK")] + public async Task Can_Be_Invariant(string culture) + { + var publishedContent = await CreatePublishedContent( + ContentVariation.Nothing, + new List + { + new() { Alias = "invariantText", Value = "The invariant content value" }, + new() { Alias = "variantText", Value = "Another invariant content value" } + }, + new List + { + new() { Alias = "invariantText", Value = "The invariant settings value" }, + new() { Alias = "variantText", Value = "Another invariant settings value" } + }); + + SetVariationContext(culture, null); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant content value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual("Another invariant content value", variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant settings value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual("Another invariant settings value", variantProperty.GetValue()); + }); + } + + [TestCase("en-US", true)] + [TestCase("da-DK", false)] + public async Task Can_Become_Variant_After_Publish(string culture, bool expectExposedBlocks) + { + var elementType = CreateElementType(ContentVariation.Nothing); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Nothing, blockListDataType); + + var content = CreateContent( + contentType, + elementType, + new List + { + new() { Alias = "invariantText", Value = "The invariant content value" }, + new() { Alias = "variantText", Value = "Another invariant content value" } + }, + new List + { + new() { Alias = "invariantText", Value = "The invariant settings value" }, + new() { Alias = "variantText", Value = "Another invariant settings value" } + }, + true); + + // the content and element types are created invariant; now make them culture variant, and enable culture variance on the "variantText" property + elementType.Variations = ContentVariation.Culture; + elementType.PropertyTypes.First(pt => pt.Alias == "variantText").Variations = ContentVariation.Culture; + ContentTypeService.Save(elementType); + + contentType.Variations = ContentVariation.Culture; + ContentTypeService.Save(contentType); + + RefreshContentTypeCache(elementType, contentType); + + // to re-publish the content in both cultures we need to set the culture names + content = ContentService.GetById(content.Key)!; + content.SetCultureName("Home (en)", "en-US"); + content.SetCultureName("Home (da)", "da-DK"); + ContentService.Save(content); + PublishContent(content, contentType); + + var publishedContent = GetPublishedContent(content.Key); + + SetVariationContext(culture, null); + + // the "blocks" property is invariant (at content level), and the block data currently stored is also invariant. + // however, the content and element types both vary by culture at this point, so the blocks should be parsed + // accordingly. this means that the block is exposed only in the default culture, and the "variantText" property + // should perform a fallback to the default language (which is en-US). + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(expectExposedBlocks ? 1 : 0, value.Count); + + if (expectExposedBlocks) + { + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.IsFalse(invariantProperty.PropertyType.VariesByCulture()); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant content value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.IsTrue(variantProperty.PropertyType.VariesByCulture()); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual("Another invariant content value", variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant settings value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual("Another invariant settings value", variantProperty.GetValue()); + }); + } + } + + [TestCase("en-US", "en-US", ContentVariation.Nothing)] + [TestCase("en-US", "en-US", ContentVariation.Culture)] + [TestCase("en-US", "da-DK", ContentVariation.Nothing)] + [TestCase("en-US", "da-DK", ContentVariation.Culture)] + [TestCase("da-DK", "en-US", ContentVariation.Nothing)] + [TestCase("da-DK", "en-US", ContentVariation.Culture)] + [TestCase("da-DK", "da-DK", ContentVariation.Nothing)] + [TestCase("da-DK", "da-DK", ContentVariation.Culture)] + public async Task Can_Become_Invariant_After_Publish(string requestCulture, string defaultCulture, ContentVariation elementVariationAfterPublish) + { + var language = await LanguageService.GetAsync(defaultCulture); + language!.IsDefault = true; + await LanguageService.UpdateAsync(language, Constants.Security.SuperUserKey); + + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent( + contentType, + elementType, + new List + { + new() { Alias = "invariantText", Value = "The invariant content value" }, + new() { Alias = "variantText", Value = "The en-US content value", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The da-DK content value", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The invariant settings value" }, + new() { Alias = "variantText", Value = "The en-US settings value", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The da-DK settings value", Culture = "da-DK" }, + }, + true); + + // the content and element types are created as variant; now update the element type according to the test case + elementType.Variations = elementVariationAfterPublish; + elementType.PropertyTypes.First(pt => pt.Alias == "variantText").Variations = elementVariationAfterPublish; + ContentTypeService.Save(elementType); + + // ...and make the content type invariant + contentType.Variations = ContentVariation.Nothing; + ContentTypeService.Save(contentType); + + RefreshContentTypeCache(elementType, contentType); + + // to re-publish the content we need to set the invariant name + content = ContentService.GetById(content.Key)!; + content.Name = "Home"; + ContentService.Save(content); + PublishContent(content, contentType); + + var publishedContent = GetPublishedContent(content.Key); + + SetVariationContext(requestCulture, null); + + // the "blocks" property is invariant (at content level), but the block data currently stored is variant because the + // content type was originally variant. however, as the content type has changed to invariant, we expect no variance + // in the rendered block output, despite the variance of the element (which may or may not vary by culture, depending + // on the test case). this means that the "variantText" property should now always output the value set for the + // default language (which is also depending on the test case). + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant content value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual($"The {defaultCulture} content value", variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant settings value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual($"The {defaultCulture} settings value", variantProperty.GetValue()); + }); + } + + [TestCase(ContentVariation.Nothing, "en-US", "English")] + [TestCase(ContentVariation.Nothing, "da-DK", "Danish")] + [TestCase(ContentVariation.Culture, "en-US", "English")] + [TestCase(ContentVariation.Culture, "da-DK", "Danish")] + [TestCase(ContentVariation.CultureAndSegment, "en-US", "English")] + [TestCase(ContentVariation.CultureAndSegment, "da-DK", "Danish")] + [TestCase(ContentVariation.Segment, "en-US", "English")] + [TestCase(ContentVariation.Segment, "da-DK", "Danish")] + public async Task Can_Handle_Both_Content_And_Element_Level_Variation(ContentVariation elementVariation, string culture, string expectedStartsWith) + { + var elementType = CreateElementType(elementVariation); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture); + + var content = CreateContent( + contentType, + elementType, + new [] + { + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "English invariant content value" }, + new() { Alias = "variantText", Value = "English variant content value", Culture = elementVariation.VariesByCulture() ? "en-US" : null } + }, + new List + { + new() { Alias = "invariantText", Value = "English invariant settings value" }, + new() { Alias = "variantText", Value = "English variant settings value", Culture = elementVariation.VariesByCulture() ? "en-US" : null } + }, + "en-US", + null), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Danish invariant content value" }, + new() { Alias = "variantText", Value = "Danish variant content value", Culture = elementVariation.VariesByCulture() ? "da-DK" : null } + }, + new List + { + new() { Alias = "invariantText", Value = "Danish invariant settings value" }, + new() { Alias = "variantText", Value = "Danish variant settings value", Culture = elementVariation.VariesByCulture() ? "da-DK" : null } + }, + "da-DK", + null) + }, + true); + + SetVariationContext(culture, null); + + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.IsTrue(invariantProperty.GetValue()!.ToString()!.StartsWith(expectedStartsWith)); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.IsTrue(variantProperty.GetValue()!.ToString()!.StartsWith(expectedStartsWith)); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.IsTrue(invariantProperty.GetValue()!.ToString()!.StartsWith(expectedStartsWith)); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.IsTrue(variantProperty.GetValue()!.ToString()!.StartsWith(expectedStartsWith)); + }); + } + + [TestCase(ContentVariation.Culture, "en-US", "Segment1")] + [TestCase(ContentVariation.Culture, "en-US", "Segment2")] + [TestCase(ContentVariation.Culture, "da-DK", "Segment1")] + [TestCase(ContentVariation.Culture, "da-DK", "Segment2")] + [TestCase(ContentVariation.CultureAndSegment, "en-US", "Segment1")] + [TestCase(ContentVariation.CultureAndSegment, "en-US", "Segment2")] + [TestCase(ContentVariation.CultureAndSegment, "da-DK", "Segment1")] + [TestCase(ContentVariation.CultureAndSegment, "da-DK", "Segment2")] + [TestCase(ContentVariation.Segment, "en-US", "Segment1")] + [TestCase(ContentVariation.Segment, "en-US", "Segment2")] + [TestCase(ContentVariation.Segment, "da-DK", "Segment1")] + [TestCase(ContentVariation.Segment, "da-DK", "Segment2")] + public async Task Can_Handle_Variant_Element_For_Invariant_Content(ContentVariation elementVariation, string culture, string segment) + { + var elementType = CreateElementType(elementVariation); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Nothing, blockListDataType); + + var content = CreateContent( + contentType, + elementType, + new [] + { + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "This is invariant content text" }, + new() { Alias = "variantText", Value = "This is also invariant content text" } + }, + new List + { + new() { Alias = "invariantText", Value = "This is invariant settings text" }, + new() { Alias = "variantText", Value = "This is also invariant settings text" } + }, + null, + null) + }, + true); + + SetVariationContext(culture, segment); + + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("This is invariant content text", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual("This is also invariant content text", variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("This is invariant settings text", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual("This is also invariant settings text", variantProperty.GetValue()); + }); + } + + [TestCase("en-US", null)] + [TestCase("en-US", "segment1")] + [TestCase("en-US", "segment2")] + [TestCase("da-DK", null)] + [TestCase("da-DK", "segment1")] + [TestCase("da-DK", "segment2")] + public async Task Can_Combine_Element_Level_Segment_Variation_With_Document_Level_Language_Variation(string culture, string? segment) + { + var elementType = CreateElementType(ContentVariation.Segment); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.CultureAndSegment, blockListDataType, ContentVariation.Culture); + + var content = CreateContent( + contentType, + elementType, + new [] + { + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "This is invariant content text for en-US" }, + new() { Alias = "variantText", Value = "This is the default segment content text for en-US", Segment = null }, + new() { Alias = "variantText", Value = "This is the segment1 segment content text for en-US", Segment = "segment1" }, + new() { Alias = "variantText", Value = "This is the segment2 segment content text for en-US", Segment = "segment2" }, + }, + new List + { + new() { Alias = "invariantText", Value = "This is invariant settings text for en-US" }, + new() { Alias = "variantText", Value = "This is the default segment settings text for en-US", Segment = null }, + new() { Alias = "variantText", Value = "This is the segment1 segment settings text for en-US", Segment = "segment1" }, + new() { Alias = "variantText", Value = "This is the segment2 segment settings text for en-US", Segment = "segment2" } + }, + "en-US", + null), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "This is invariant content text for da-DK" }, + new() { Alias = "variantText", Value = "This is the default segment content text for da-DK", Segment = null }, + new() { Alias = "variantText", Value = "This is the segment1 segment content text for da-DK", Segment = "segment1" }, + new() { Alias = "variantText", Value = "This is the segment2 segment content text for da-DK", Segment = "segment2" }, + }, + new List + { + new() { Alias = "invariantText", Value = "This is invariant settings text for da-DK" }, + new() { Alias = "variantText", Value = "This is the default segment settings text for da-DK", Segment = null }, + new() { Alias = "variantText", Value = "This is the segment1 segment settings text for da-DK", Segment = "segment1" }, + new() { Alias = "variantText", Value = "This is the segment2 segment settings text for da-DK", Segment = "segment2" } + }, + "da-DK", + null) + }, + true); + + SetVariationContext(culture, segment); + + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual(ContentVariation.Nothing, invariantProperty.PropertyType.Variations); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual($"This is invariant content text for {culture}", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual(ContentVariation.Segment, variantProperty.PropertyType.Variations); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual( + segment is null ? $"This is the default segment content text for {culture}" : $"This is the {segment} segment content text for {culture}", + variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual(ContentVariation.Nothing, invariantProperty.PropertyType.Variations); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual($"This is invariant settings text for {culture}", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual(ContentVariation.Segment, variantProperty.PropertyType.Variations); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual( + segment is null ? $"This is the default segment settings text for {culture}" : $"This is the {segment} segment settings text for {culture}", + variantProperty.GetValue()); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs new file mode 100644 index 0000000000..29131e4fa6 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs @@ -0,0 +1,1551 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +/* +If the content type varies, then: + +1. Variant element types ALWAYS adds variation to the block property Expose, as well as to any variant element properties. +2. Invariant element types NEVER adds variation to the block property Expose, nor to any variant element properties (because there are none). + +If the content type does NOT vary, then variation is NEVER added to Expose, nor to any variant properties - regardless of the element type variation. + +This means that an invariant element cannot be "turned off" for a single variation - it's all or nothing. + +It also means that in a variant setting, the parent property variance has no effect for the variance notation for any nested blocks. +*/ +public partial class BlockListElementLevelVariationTests +{ + [Test] + public async Task Can_Publish_Cultures_Independently_Invariant_Blocks() + { + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent( + contentType, + elementType, + new List + { + new() { Alias = "invariantText", Value = "The first invariant content value" }, + new() { Alias = "variantText", Value = "The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The first invariant settings value" }, + new() { Alias = "variantText", Value = "The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first settings value in Danish", Culture = "da-DK" }, + }, + true); + + AssertPropertyValues("en-US", + "The first invariant content value", "The first content value in English", + "The first invariant settings value", "The first settings value in English"); + + AssertPropertyValues("da-DK", + "The first invariant content value", "The first content value in Danish", + "The first invariant settings value", "The first settings value in Danish"); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + blockListValue.ContentData[0].Values[0].Value = "The second invariant content value"; + blockListValue.ContentData[0].Values[1].Value = "The second content value in English"; + blockListValue.ContentData[0].Values[2].Value = "The second content value in Danish"; + blockListValue.SettingsData[0].Values[0].Value = "The second invariant settings value"; + blockListValue.SettingsData[0].Values[1].Value = "The second settings value in English"; + blockListValue.SettingsData[0].Values[2].Value = "The second settings value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues("en-US", + "The second invariant content value", "The second content value in English", + "The second invariant settings value", "The second settings value in English"); + + AssertPropertyValues("da-DK", + "The second invariant content value", "The first content value in Danish", + "The second invariant settings value", "The first settings value in Danish"); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", + "The second invariant content value", "The second content value in Danish", + "The second invariant settings value", "The second settings value in Danish"); + + void AssertPropertyValues(string culture, + string expectedInvariantContentValue, string expectedVariantContentValue, + string expectedInvariantSettingsValue, string expectedVariantSettingsValue) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantContentValue, blockListItem.Content.Value("invariantText")); + Assert.AreEqual(expectedVariantContentValue, blockListItem.Content.Value("variantText")); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantSettingsValue, blockListItem.Settings.Value("invariantText")); + Assert.AreEqual(expectedVariantSettingsValue, blockListItem.Settings.Value("variantText")); + }); + } + } + + [Test] + public async Task Can_Publish_Cultures_Independently_Variant_Blocks() + { + var elementType = CreateElementType(ContentVariation.Nothing); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture); + + var content = CreateContent( + contentType, + elementType, + new [] + { + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "English invariantText content value" }, + new() { Alias = "variantText", Value = "English variantText content value" } + }, + new List + { + new() { Alias = "invariantText", Value = "English invariantText settings value" }, + new() { Alias = "variantText", Value = "English variantText settings value" } + }, + "en-US", + null), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Danish invariantText content value" }, + new() { Alias = "variantText", Value = "Danish variantText content value" } + }, + new List + { + new() { Alias = "invariantText", Value = "Danish invariantText settings value" }, + new() { Alias = "variantText", Value = "Danish variantText settings value" } + }, + "da-DK", + null) + }, + true); + + AssertPropertyValues("en-US", + "English invariantText content value", "English variantText content value", + "English invariantText settings value", "English variantText settings value"); + + AssertPropertyValues("da-DK", + "Danish invariantText content value", "Danish variantText content value", + "Danish invariantText settings value", "Danish variantText settings value"); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue("en-US")!); + blockListValue.ContentData[0].Values[0].Value = "English invariantText content value (updated)"; + blockListValue.ContentData[0].Values[1].Value = "English variantText content value (updated)"; + blockListValue.SettingsData[0].Values[0].Value = "English invariantText settings value (updated)"; + blockListValue.SettingsData[0].Values[1].Value = "English variantText settings value (updated)"; + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue), "en-US"); + + blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue("da-DK")!); + blockListValue.ContentData[0].Values[0].Value = "Danish invariantText content value (updated)"; + blockListValue.ContentData[0].Values[1].Value = "Danish variantText content value (updated)"; + blockListValue.SettingsData[0].Values[0].Value = "Danish invariantText settings value (updated)"; + blockListValue.SettingsData[0].Values[1].Value = "Danish variantText settings value (updated)"; + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue), "da-DK"); + + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues("en-US", + "English invariantText content value (updated)", "English variantText content value (updated)", + "English invariantText settings value (updated)", "English variantText settings value (updated)"); + + AssertPropertyValues("da-DK", + "Danish invariantText content value", "Danish variantText content value", + "Danish invariantText settings value", "Danish variantText settings value"); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", + "Danish invariantText content value (updated)", "Danish variantText content value (updated)", + "Danish invariantText settings value (updated)", "Danish variantText settings value (updated)"); + + void AssertPropertyValues(string culture, + string expectedInvariantContentValue, string expectedVariantContentValue, + string expectedInvariantSettingsValue, string expectedVariantSettingsValue) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantContentValue, blockListItem.Content.Value("invariantText")); + Assert.AreEqual(expectedVariantContentValue, blockListItem.Content.Value("variantText")); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantSettingsValue, blockListItem.Settings.Value("invariantText")); + Assert.AreEqual(expectedVariantSettingsValue, blockListItem.Settings.Value("variantText")); + }); + } + } + + [Test] + public async Task Can_Publish_Cultures_Independently_Nested_Invariant_Blocks() + { + var nestedElementType = CreateElementType(ContentVariation.Culture); + var nestedBlockListDataType = await CreateBlockListDataType(nestedElementType); + + var rootElementType = new ContentTypeBuilder() + .WithAlias("myRootElementType") + .WithName("My Root Element Type") + .WithIsElement(true) + .WithContentVariation(ContentVariation.Culture) + .AddPropertyType() + .WithAlias("invariantText") + .WithName("Invariant text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Nothing) + .Done() + .AddPropertyType() + .WithAlias("variantText") + .WithName("Variant text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Culture) + .Done() + .AddPropertyType() + .WithAlias("nestedBlocks") + .WithName("Nested blocks") + .WithDataTypeId(nestedBlockListDataType.Id) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.BlockList) + .WithValueStorageType(ValueStorageType.Ntext) + .WithVariations(ContentVariation.Nothing) + .Done() + .Build(); + ContentTypeService.Save(rootElementType); + var rootBlockListDataType = await CreateBlockListDataType(rootElementType); + var contentType = CreateContentType(ContentVariation.Culture, rootBlockListDataType); + + var nestedElementContentKey = Guid.NewGuid(); + var nestedElementSettingsKey = Guid.NewGuid(); + var content = CreateContent( + contentType, + rootElementType, + new List + { + new() + { + Alias = "nestedBlocks", + Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKey, + nestedElementSettingsKey, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant content value" }, + new() { Alias = "variantText", Value = "The first nested content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first nested content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant settings value" }, + new() { Alias = "variantText", Value = "The first nested settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first nested settings value in Danish", Culture = "da-DK" }, + }, + null, + null)) + }, + new() { Alias = "invariantText", Value = "The first root invariant content value" }, + new() { Alias = "variantText", Value = "The first root content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first root content value in Danish", Culture = "da-DK" }, + }, + [], + true); + + AssertPropertyValues( + "en-US", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The first root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The first root content value in English", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The first nested content value in English", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The first nested settings value in English", nestedBlockSetting.Value("variantText")); + }); + + AssertPropertyValues( + "da-DK", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The first root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The first root content value in Danish", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The first nested content value in Danish", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The first nested settings value in Danish", nestedBlockSetting.Value("variantText")); + }); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + blockListValue.ContentData[0].Values[0].Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKey, + nestedElementSettingsKey, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant content value" }, + new() { Alias = "variantText", Value = "The second nested content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The second nested content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant settings value" }, + new() { Alias = "variantText", Value = "The second nested settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The second nested settings value in Danish", Culture = "da-DK" }, + }, + null, + null)); + blockListValue.ContentData[0].Values[1].Value = "The second root invariant content value"; + blockListValue.ContentData[0].Values[2].Value = "The second root content value in English"; + blockListValue.ContentData[0].Values[3].Value = "The second root content value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues( + "en-US", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The second root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The second root content value in English", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The second nested content value in English", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The second nested settings value in English", nestedBlockSetting.Value("variantText")); + }); + + AssertPropertyValues( + "da-DK", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The second root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The first root content value in Danish", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The first nested content value in Danish", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The first nested settings value in Danish", nestedBlockSetting.Value("variantText")); + }); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues( + "da-DK", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The second root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The second root content value in Danish", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The second nested content value in Danish", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The second nested settings value in Danish", nestedBlockSetting.Value("variantText")); + }); + + void AssertPropertyValues(string culture, Action validateBlockValues) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var rootBlock = publishedContent.Value("blocks"); + Assert.IsNotNull(rootBlock); + Assert.AreEqual(1, rootBlock.Count); + Assert.Multiple(() => + { + var rootBlockContent = rootBlock.First().Content; + + var nestedBlock = rootBlockContent.Value("nestedBlocks"); + Assert.IsNotNull(nestedBlock); + Assert.AreEqual(1, nestedBlock.Count); + + var nestedBlockContent = nestedBlock.First().Content; + var nestedBlockSettings = nestedBlock.First().Settings; + + validateBlockValues(rootBlockContent, nestedBlockContent, nestedBlockSettings); + }); + } + } + + [Test] + public async Task Can_Publish_Cultures_Independently_Nested_Variant_Blocks() + { + var nestedElementType = CreateElementType(ContentVariation.Nothing); + var nestedBlockListDataType = await CreateBlockListDataType(nestedElementType); + + var rootElementType = new ContentTypeBuilder() + .WithAlias("myRootElementType") + .WithName("My Root Element Type") + .WithIsElement(true) + .WithContentVariation(ContentVariation.Culture) + .AddPropertyType() + .WithAlias("invariantText") + .WithName("Invariant text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Nothing) + .Done() + .AddPropertyType() + .WithAlias("variantText") + .WithName("Variant text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Culture) + .Done() + .AddPropertyType() + .WithAlias("nestedBlocks") + .WithName("Nested blocks") + .WithDataTypeId(nestedBlockListDataType.Id) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.BlockList) + .WithValueStorageType(ValueStorageType.Ntext) + .WithVariations(ContentVariation.Culture) + .Done() + .Build(); + ContentTypeService.Save(rootElementType); + var rootBlockListDataType = await CreateBlockListDataType(rootElementType); + var contentType = CreateContentType(ContentVariation.Culture, rootBlockListDataType); + + var nestedElementContentKeyEnUs = Guid.NewGuid(); + var nestedElementSettingsKeyEnUs = Guid.NewGuid(); + var nestedElementContentKeyDaDk = Guid.NewGuid(); + var nestedElementSettingsKeyDaDk = Guid.NewGuid(); + var content = CreateContent( + contentType, + rootElementType, + new List + { + new() + { + Alias = "nestedBlocks", + Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKeyEnUs, + nestedElementSettingsKeyEnUs, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant content value" }, + new() { Alias = "variantText", Value = "The first nested content value in English" } + }, + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant settings value" }, + new() { Alias = "variantText", Value = "The first nested settings value in English" } + }, + null, + null)), + Culture = "en-US" + }, + new() + { + Alias = "nestedBlocks", + Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKeyDaDk, + nestedElementSettingsKeyDaDk, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant content value" }, + new() { Alias = "variantText", Value = "The first nested content value in Danish" } + }, + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant settings value" }, + new() { Alias = "variantText", Value = "The first nested settings value in Danish" } + }, + null, + null)), + Culture = "da-DK" + }, + new() { Alias = "invariantText", Value = "The first root invariant content value" }, + new() { Alias = "variantText", Value = "The first root content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first root content value in Danish", Culture = "da-DK" }, + }, + [], + true); + + AssertPropertyValues( + "en-US", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The first root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The first root content value in English", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The first nested content value in English", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The first nested settings value in English", nestedBlockSetting.Value("variantText")); + }); + + AssertPropertyValues( + "da-DK", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The first root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The first root content value in Danish", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The first nested content value in Danish", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The first nested settings value in Danish", nestedBlockSetting.Value("variantText")); + }); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + blockListValue.ContentData[0].Values[0].Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKeyEnUs, + nestedElementSettingsKeyEnUs, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant content value" }, + new() { Alias = "variantText", Value = "The second nested content value in English" } + }, + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant settings value" }, + new() { Alias = "variantText", Value = "The second nested settings value in English" } + }, + null, + null)); + blockListValue.ContentData[0].Values[1].Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKeyDaDk, + nestedElementSettingsKeyDaDk, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant content value" }, + new() { Alias = "variantText", Value = "The second nested content value in Danish" } + }, + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant settings value" }, + new() { Alias = "variantText", Value = "The second nested settings value in Danish" } + }, + null, + null)); + blockListValue.ContentData[0].Values[2].Value = "The second root invariant content value"; + blockListValue.ContentData[0].Values[3].Value = "The second root content value in English"; + blockListValue.ContentData[0].Values[4].Value = "The second root content value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues( + "en-US", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The second root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The second root content value in English", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The second nested content value in English", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The second nested settings value in English", nestedBlockSetting.Value("variantText")); + }); + + AssertPropertyValues( + "da-DK", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The second root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The first root content value in Danish", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The first nested content value in Danish", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The first nested settings value in Danish", nestedBlockSetting.Value("variantText")); + }); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues( + "da-DK", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The second root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The second root content value in Danish", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The second nested content value in Danish", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The second nested settings value in Danish", nestedBlockSetting.Value("variantText")); + }); + + void AssertPropertyValues(string culture, Action validateBlockValues) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var rootBlock = publishedContent.Value("blocks"); + Assert.IsNotNull(rootBlock); + Assert.AreEqual(1, rootBlock.Count); + Assert.Multiple(() => + { + var rootBlockContent = rootBlock.First().Content; + + var nestedBlock = rootBlockContent.Value("nestedBlocks"); + Assert.IsNotNull(nestedBlock); + Assert.AreEqual(1, nestedBlock.Count); + + var nestedBlockContent = nestedBlock.First().Content; + var nestedBlockSettings = nestedBlock.First().Settings; + + validateBlockValues(rootBlockContent, nestedBlockContent, nestedBlockSettings); + }); + } + } + + [Test] + public async Task Can_Publish_Cultures_Independently_With_Segments() + { + var elementType = CreateElementType(ContentVariation.CultureAndSegment); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.CultureAndSegment, blockListDataType); + + var content = CreateContent( + contentType, + elementType, + new List + { + new() { Alias = "invariantText", Value = "The first invariant content value" }, + new() { Alias = "variantText", Value = "The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first content value in English (Segment 1)", Culture = "en-US", Segment = "s1" }, + new() { Alias = "variantText", Value = "The first content value in English (Segment 2)", Culture = "en-US", Segment = "s2" }, + new() { Alias = "variantText", Value = "The first content value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "The first content value in Danish (Segment 1)", Culture = "da-DK", Segment = "s1" }, + new() { Alias = "variantText", Value = "The first content value in Danish (Segment 2)", Culture = "da-DK", Segment = "s2" }, + }, + [], + true); + + AssertPropertyValues("en-US", null, + "The first invariant content value", "The first content value in English"); + + AssertPropertyValues("en-US", "s1", + "The first invariant content value", "The first content value in English (Segment 1)"); + + AssertPropertyValues("en-US", "s2", + "The first invariant content value", "The first content value in English (Segment 2)"); + + AssertPropertyValues("da-DK", null, + "The first invariant content value", "The first content value in Danish"); + + AssertPropertyValues("da-DK", "s1", + "The first invariant content value", "The first content value in Danish (Segment 1)"); + + AssertPropertyValues("da-DK", "s2", + "The first invariant content value", "The first content value in Danish (Segment 2)"); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + blockListValue.ContentData[0].Values[0].Value = "The second invariant content value"; + blockListValue.ContentData[0].Values[1].Value = "The second content value in English"; + blockListValue.ContentData[0].Values[2].Value = "The second content value in English (Segment 1)"; + blockListValue.ContentData[0].Values[3].Value = "The second content value in English (Segment 2)"; + blockListValue.ContentData[0].Values[4].Value = "The second content value in Danish"; + blockListValue.ContentData[0].Values[5].Value = "The second content value in Danish (Segment 1)"; + blockListValue.ContentData[0].Values[6].Value = "The second content value in Danish (Segment 2)"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues("en-US", null, + "The second invariant content value", "The second content value in English"); + + AssertPropertyValues("en-US", "s1", + "The second invariant content value", "The second content value in English (Segment 1)"); + + AssertPropertyValues("en-US", "s2", + "The second invariant content value", "The second content value in English (Segment 2)"); + + AssertPropertyValues("da-DK", null, + "The second invariant content value", "The first content value in Danish"); + + AssertPropertyValues("da-DK", "s1", + "The second invariant content value", "The first content value in Danish (Segment 1)"); + + AssertPropertyValues("da-DK", "s2", + "The second invariant content value", "The first content value in Danish (Segment 2)"); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", null, + "The second invariant content value", "The second content value in Danish"); + + AssertPropertyValues("da-DK", "s1", + "The second invariant content value", "The second content value in Danish (Segment 1)"); + + AssertPropertyValues("da-DK", "s2", + "The second invariant content value", "The second content value in Danish (Segment 2)"); + + void AssertPropertyValues(string culture, string? segment, string expectedInvariantContentValue, string expectedVariantContentValue) + { + SetVariationContext(culture, segment); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantContentValue, blockListItem.Content.Value("invariantText")); + Assert.AreEqual(expectedVariantContentValue, blockListItem.Content.Value("variantText")); + }); + } + } + + [Test] + public async Task Can_Publish_With_Segments() + { + var elementType = CreateElementType(ContentVariation.Segment); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Segment, blockListDataType); + + var content = CreateContent( + contentType, + elementType, + new List + { + new() { Alias = "invariantText", Value = "The first invariant content value" }, + new() { Alias = "variantText", Value = "The first content value" }, + new() { Alias = "variantText", Value = "The first content value (Segment 1)", Segment = "s1" }, + new() { Alias = "variantText", Value = "The first content value (Segment 2)", Segment = "s2" } + }, + [], + true); + + AssertPropertyValues(null, "The first invariant content value", "The first content value"); + + AssertPropertyValues("s1", "The first invariant content value", "The first content value (Segment 1)"); + + AssertPropertyValues("s2", "The first invariant content value", "The first content value (Segment 2)"); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + blockListValue.ContentData[0].Values[0].Value = "The second invariant content value"; + blockListValue.ContentData[0].Values[1].Value = "The second content value"; + blockListValue.ContentData[0].Values[2].Value = "The second content value (Segment 1)"; + blockListValue.ContentData[0].Values[3].Value = "The second content value (Segment 2)"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType); + + AssertPropertyValues(null, "The second invariant content value", "The second content value"); + + AssertPropertyValues("s1", "The second invariant content value", "The second content value (Segment 1)"); + + AssertPropertyValues("s2", "The second invariant content value", "The second content value (Segment 2)"); + + void AssertPropertyValues(string? segment, string expectedInvariantContentValue, string expectedVariantContentValue) + { + SetVariationContext(null, segment); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantContentValue, blockListItem.Content.Value("invariantText")); + Assert.AreEqual(expectedVariantContentValue, blockListItem.Content.Value("variantText")); + }); + } + } + + [Test] + public async Task Can_Publish_With_Blocks_Removed() + { + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The first invariant content value" }, + new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ), + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#2: The first invariant content value" }, + new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ), + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#3: The first invariant content value" }, + new() { Alias = "variantText", Value = "#3: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#3: The first content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ), + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 3, blocks => + { + Assert.AreEqual("#1: The first invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in English", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The first invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The first content value in English", blocks[1].Content.Value("variantText")); + + Assert.AreEqual("#3: The first invariant content value", blocks[2].Content.Value("invariantText")); + Assert.AreEqual("#3: The first content value in English", blocks[2].Content.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 3, blocks => + { + Assert.AreEqual("#1: The first invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The first invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The first content value in Danish", blocks[1].Content.Value("variantText")); + + Assert.AreEqual("#3: The first invariant content value", blocks[2].Content.Value("invariantText")); + Assert.AreEqual("#3: The first content value in Danish", blocks[2].Content.Value("variantText")); + }); + + // remove block #2 + blockListValue.Layout[blockListValue.Layout.First().Key] = + [ + blockListValue.Layout.First().Value.First(), + blockListValue.Layout.First().Value.Last() + ]; + blockListValue.ContentData.RemoveAt(1); + blockListValue.SettingsData.RemoveAt(1); + + blockListValue.ContentData[0].Values[0].Value = "#1: The second invariant content value"; + blockListValue.ContentData[0].Values[1].Value = "#1: The second content value in English"; + blockListValue.ContentData[0].Values[2].Value = "#1: The second content value in Danish"; + blockListValue.ContentData[1].Values[0].Value = "#3: The second invariant content value"; + blockListValue.ContentData[1].Values[1].Value = "#3: The second content value in English"; + blockListValue.ContentData[1].Values[2].Value = "#3: The second content value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in English", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#3: The second content value in English", blocks[1].Content.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#3: The first content value in Danish", blocks[1].Content.Value("variantText")); + }); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in Danish", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#3: The second content value in Danish", blocks[1].Content.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + [Test] + public async Task Can_Publish_With_Blocks_In_One_Language() + { + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var firstBlockContentElementKey = Guid.NewGuid(); + var firstBlockSettingsElementKey = Guid.NewGuid(); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + firstBlockContentElementKey, + firstBlockSettingsElementKey, + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The first invariant content value" }, + new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ) + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 1, blocks => + { + Assert.AreEqual("#1: The first invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in English", blocks[0].Content.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("#1: The first invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Value("variantText")); + }); + + // Add one more block + blockListValue = BlockListPropertyValue( + elementType, + [ + ( + firstBlockContentElementKey, + firstBlockSettingsElementKey, + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The second invariant content value" }, + new() { Alias = "variantText", Value = "#1: The second content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The second content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ), + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#2: The second invariant content value" }, + new() { Alias = "variantText", Value = "#2: The second content value in English", Culture = "en-US" } + }, + [], + null, + null + ) + ) + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in English", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The second content value in English", blocks[1].Content.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Value("variantText")); + }); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in Danish", blocks[0].Content.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + [Test] + public async Task Can_Publish_With_Blocks_Exposed() + { + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The invariant content value" }, + new() { Alias = "variantText", Value = "#1: The content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ), + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#2: The invariant content value" }, + new() { Alias = "variantText", Value = "#2: The content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ) + ] + ); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "da-DK" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.AreEqual("#1: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The content value in English", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The content value in English", blocks[1].Content.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("#2: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#2: The content value in Danish", blocks[0].Content.Value("variantText")); + }); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "da-DK" }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "da-DK" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.AreEqual("#1: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The content value in Danish", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The content value in Danish", blocks[1].Content.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + [Test] + public async Task Can_Expose_Invariant_Blocks_Across_Cultures() + { + var elementType = CreateElementType(ContentVariation.Nothing); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The invariant content value" }, + new() { Alias = "variantText", Value = "#1: The other invariant content value" } + }, + [], + null, + null + ) + ), + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#2: The invariant content value" }, + new() { Alias = "variantText", Value = "#2: The other invariant content value" } + }, + [], + null, + null + ) + ) + ] + ); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key }, + new() { ContentKey = blockListValue.ContentData[1].Key }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US", "da-DK"]); + + foreach (var culture in new[] { "en-US", "da-DK" }) + { + AssertPropertyValues(culture, 2, blocks => + { + Assert.AreEqual("#1: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The other invariant content value", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The other invariant content value", blocks[1].Content.Value("variantText")); + }); + } + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[1].Key }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + // note how publishing in one language affects both due to the invariance of the block element type + PublishContent(content, contentType, ["en-US"]); + + foreach (var culture in new[] { "en-US", "da-DK" }) + { + AssertPropertyValues(culture, 1, blocks => + { + Assert.AreEqual("#2: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#2: The other invariant content value", blocks[0].Content.Value("variantText")); + }); + } + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + [Test] + public async Task Can_Expose_Both_Variant_And_Invariant_Blocks() + { + var invariantElementType = CreateElementType(ContentVariation.Nothing); + var variantElementType = CreateElementType(ContentVariation.Culture, "myVariantElementType"); + var blockListDataType = await CreateBlockEditorDataType( + Constants.PropertyEditors.Aliases.BlockList, + new BlockListConfiguration.BlockConfiguration[] + { + new() { ContentElementTypeKey = invariantElementType.Key }, + new() { ContentElementTypeKey = variantElementType.Key } + }); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, invariantElementType, [], false); + var blockListValue = BlockListPropertyValue( + invariantElementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The invariant content value" }, + new() { Alias = "variantText", Value = "#1: The other invariant content value" } + }, + [], + null, + null + ) + ) + ] + ); + + var variantElementKey = Guid.NewGuid(); + blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList] = blockListValue + .Layout[Constants.PropertyEditors.Aliases.BlockList] + .Union(new[] { new BlockListLayoutItem(variantElementKey) }); + blockListValue.ContentData.Add( + new BlockItemData(variantElementKey, variantElementType.Key, variantElementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#2: The invariant content value" }, + new() { Alias = "variantText", Value = "#2: The variant content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The variant content value in Danish", Culture = "da-DK" }, + ] + } + ); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "en-US" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.AreEqual("#1: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The other invariant content value", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The variant content value in English", blocks[1].Content.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("#1: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The other invariant content value", blocks[0].Content.Value("variantText")); + }); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "da-DK" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.AreEqual("#1: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The other invariant content value", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The variant content value in Danish", blocks[1].Content.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + [Test] + public async Task Can_Publish_Invariant_Properties_Without_Default_Culture_With_AllowEditInvariantFromNonDefault() + { + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "The first invariant content value" }, + new() { Alias = "variantText", Value = "The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first content value in Danish", Culture = "da-DK" } + }, + new List + { + new() { Alias = "invariantText", Value = "The first invariant settings value" }, + new() { Alias = "variantText", Value = "The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first settings value in Danish", Culture = "da-DK" }, + }, + null, + null + ) + ) + ] + ); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "da-DK" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("en-US", 0); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("The first invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("The first content value in Danish", blocks[0].Content.Value("variantText")); + Assert.IsNotNull(blocks[0].Settings); + Assert.AreEqual("The first invariant settings value", blocks[0].Settings.Value("invariantText")); + Assert.AreEqual("The first settings value in Danish", blocks[0].Settings.Value("variantText")); + }); + + blockListValue.ContentData[0].Values[0].Value = "The second invariant content value"; + blockListValue.ContentData[0].Values[1].Value = "The second content value in English"; + blockListValue.ContentData[0].Values[2].Value = "The second content value in Danish"; + blockListValue.SettingsData[0].Values[0].Value = "The second invariant settings value"; + blockListValue.SettingsData[0].Values[1].Value = "The second settings value in English"; + blockListValue.SettingsData[0].Values[2].Value = "The second settings value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("en-US", 0); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("The second content value in Danish", blocks[0].Content.Value("variantText")); + Assert.IsNotNull(blocks[0].Settings); + Assert.AreEqual("The second invariant settings value", blocks[0].Settings.Value("invariantText")); + Assert.AreEqual("The second settings value in Danish", blocks[0].Settings.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action? validateBlocks = null) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks?.Invoke(value); + } + } + + [Test] + public async Task Cannot_Publish_Invariant_Properties_Without_Default_Culture_Without_AllowEditInvariantFromNonDefault() + { + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "The first invariant content value" }, + new() { Alias = "variantText", Value = "The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first content value in Danish", Culture = "da-DK" } + }, + new List + { + new() { Alias = "invariantText", Value = "The first invariant settings value" }, + new() { Alias = "variantText", Value = "The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first settings value in Danish", Culture = "da-DK" }, + }, + null, + null + ) + ) + ] + ); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "da-DK" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("en-US", 0); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual(string.Empty, blocks[0].Content.Value("invariantText")); + Assert.AreEqual("The first content value in Danish", blocks[0].Content.Value("variantText")); + Assert.IsNotNull(blocks[0].Settings); + Assert.AreEqual(string.Empty, blocks[0].Settings.Value("invariantText")); + Assert.AreEqual("The first settings value in Danish", blocks[0].Settings.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action? validateBlocks = null) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks?.Invoke(value); + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs new file mode 100644 index 0000000000..8a798a603c --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs @@ -0,0 +1,167 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +public partial class BlockListElementLevelVariationTests : BlockEditorElementVariationTestBase +{ + [OneTimeSetUp] + public void OneTimeSetUp() + { + TestsRequiringAllowEditInvariantFromNonDefault.Add(nameof(Can_Publish_Invariant_Properties_Without_Default_Culture_With_AllowEditInvariantFromNonDefault)); + } + + private IJsonSerializer JsonSerializer => GetRequiredService(); + + private async Task CreateBlockListDataType(IContentType elementType) + => await CreateBlockEditorDataType( + Constants.PropertyEditors.Aliases.BlockList, + new BlockListConfiguration.BlockConfiguration[] + { + new() { ContentElementTypeKey = elementType.Key, SettingsElementTypeKey = elementType.Key } + }); + + private IContent CreateContent(IContentType contentType, IContentType elementType, IList blockContentValues, IList blockSettingsValues, bool publishContent) + => CreateContent( + contentType, + elementType, + new[] { new BlockProperty(blockContentValues, blockSettingsValues, null, null) }, + publishContent); + + private IContent CreateContent(IContentType contentType, IContentType elementType, IEnumerable blocksProperties, bool publishContent) + { + var contentBuilder = new ContentBuilder() + .WithContentType(contentType); + contentBuilder = contentType.VariesByCulture() + ? contentBuilder + .WithCultureName("en-US", "Home (en)") + .WithCultureName("da-DK", "Home (da)") + : contentBuilder.WithName("Home"); + + var content = contentBuilder.Build(); + + var contentElementKey = Guid.NewGuid(); + var settingsElementKey = Guid.NewGuid(); + foreach (var blocksProperty in blocksProperties) + { + var blockListValue = BlockListPropertyValue(elementType, contentElementKey, settingsElementKey, blocksProperty); + var propertyValue = JsonSerializer.Serialize(blockListValue); + content.Properties["blocks"]!.SetValue(propertyValue, blocksProperty.Culture, blocksProperty.Segment); + } + + ContentService.Save(content); + + if (publishContent) + { + PublishContent(content, contentType); + } + + return content; + } + + private BlockListValue BlockListPropertyValue(IContentType elementType, Guid contentElementKey, Guid settingsElementKey, BlockProperty blocksProperty) + => BlockListPropertyValue(elementType, [(contentElementKey, settingsElementKey, blocksProperty)]); + + private BlockListValue BlockListPropertyValue(IContentType elementType, List<(Guid contentElementKey, Guid settingsElementKey, BlockProperty BlocksProperty)> blocks) + { + var expose = new List(); + foreach (var block in blocks) + { + var cultures = elementType.VariesByCulture() + ? new[] { block.BlocksProperty.Culture } + .Union(block.BlocksProperty.BlockContentValues.Select(value => value.Culture)) + .WhereNotNull() + .Distinct() + .ToArray() + : [null]; + if (cultures.Any() is false) + { + cultures = [null]; + } + + var segments = elementType.VariesBySegment() + ? new[] { block.BlocksProperty.Segment } + .Union(block.BlocksProperty.BlockContentValues.Select(value => value.Segment)) + .Distinct() + .ToArray() + : [null]; + + expose.AddRange(cultures.SelectMany(culture => segments.Select(segment => + new BlockItemVariation(block.contentElementKey, culture, segment)))); + } + + return new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockList, + blocks.Select(block => new BlockListLayoutItem + { + ContentKey = block.contentElementKey, SettingsKey = block.settingsElementKey + }).ToArray() + } + }, + ContentData = + blocks.Select(block => new BlockItemData + { + Key = block.contentElementKey, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + Values = block.BlocksProperty.BlockContentValues + }).ToList(), + SettingsData = blocks.Select(block => new BlockItemData + { + Key = block.settingsElementKey, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + Values = block.BlocksProperty.BlockSettingsValues + }).ToList(), + Expose = expose + }; + } + + private void PublishContent(IContent content, IContentType contentType, string[]? culturesToPublish = null) + { + culturesToPublish ??= contentType.VariesByCulture() + ? ["en-US", "da-DK"] + : ["*"]; + PublishContent(content, culturesToPublish); + } + + private async Task CreatePublishedContent(ContentVariation variation, IList blockContentValues, IList blockSettingsValues) + { + var elementType = CreateElementType(variation); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(variation, blockListDataType); + + var content = CreateContent(contentType, elementType, blockContentValues, blockSettingsValues, true); + return GetPublishedContent(content.Key); + } + + private class BlockProperty + { + public BlockProperty(IList blockContentValues, IList blockSettingsValues, string? culture, string? segment) + { + BlockContentValues = blockContentValues; + BlockSettingsValues = blockSettingsValues; + Culture = culture; + Segment = segment; + } + + public IList BlockContentValues { get; } + + public IList BlockSettingsValues { get; } + + public string? Culture { get; } + + public string? Segment { get; } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorTests.cs new file mode 100644 index 0000000000..0b10332352 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorTests.cs @@ -0,0 +1,365 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class BlockListPropertyEditorTests : UmbracoIntegrationTest +{ + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + private IDataTypeService DataTypeService => GetRequiredService(); + + private IJsonSerializer JsonSerializer => GetRequiredService(); + + private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService(); + + private PropertyEditorCollection PropertyEditorCollection => GetRequiredService(); + + [Test] + public async Task Can_Track_References() + { + var textPageContentType = ContentTypeBuilder.CreateTextPageContentType("myContentType"); + textPageContentType.AllowedTemplates = Enumerable.Empty(); + ContentTypeService.Save(textPageContentType); + + var textPage = ContentBuilder.CreateTextpageContent(textPageContentType, "My Picked Content", -1); + ContentService.Save(textPage); + + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + ContentTypeService.Save(elementType); + + var blockListContentType = await CreateBlockListContentType(elementType); + + var contentElementKey = Guid.NewGuid(); + var blockListValue = new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockList, + new IBlockLayoutItem[] + { + new BlockListLayoutItem { ContentKey = contentElementKey } + } + } + }, + ContentData = + [ + new() + { + Key = contentElementKey, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + Values = + [ + new () + { + Alias = "contentPicker", + Value = textPage.GetUdi() + } + ] + } + ] + }; + var blocksPropertyValue = JsonSerializer.Serialize(blockListValue); + + var content = new ContentBuilder() + .WithContentType(blockListContentType) + .WithName("My Blocks") + .WithPropertyValues(new { blocks = blocksPropertyValue }) + .Build(); + ContentService.Save(content); + + var valueEditor = await GetValueEditor(blockListContentType); + + var references = valueEditor.GetReferences(content.GetValue("blocks")).ToArray(); + Assert.AreEqual(1, references.Length); + var reference = references.First(); + Assert.AreEqual(Constants.Conventions.RelationTypes.RelatedDocumentAlias, reference.RelationTypeAlias); + Assert.AreEqual(textPage.GetUdi(), reference.Udi); + } + + [Test] + public async Task Can_Track_Tags() + { + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + ContentTypeService.Save(elementType); + + var blockListContentType = await CreateBlockListContentType(elementType); + + var contentElementKey = Guid.NewGuid(); + var blockListValue = new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockList, + new IBlockLayoutItem[] + { + new BlockListLayoutItem { ContentKey = contentElementKey } + } + } + }, + ContentData = + [ + new() + { + Key = contentElementKey, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + Values = + [ + new () + { + Alias = "tags", + // this is a little skewed, but the tags editor expects a serialized array of strings + Value = JsonSerializer.Serialize(new[] { "Tag One", "Tag Two", "Tag Three" }) + } + ] + } + ] + }; + var blocksPropertyValue = JsonSerializer.Serialize(blockListValue); + + var content = new ContentBuilder() + .WithContentType(blockListContentType) + .WithName("My Blocks") + .WithPropertyValues(new { blocks = blocksPropertyValue }) + .Build(); + ContentService.Save(content); + + var valueEditor = await GetValueEditor(blockListContentType); + + var tags = valueEditor.GetTags(content.GetValue("blocks"), null, null).ToArray(); + Assert.AreEqual(3, tags.Length); + Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag One")); + Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Two")); + Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Three")); + } + + [Test] + public async Task Can_Handle_Culture_Variance_Addition() + { + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + ContentTypeService.Save(elementType); + + var blockListContentType = await CreateBlockListContentType(elementType); + + var contentElementKey = Guid.NewGuid(); + var blockListValue = new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockList, + new IBlockLayoutItem[] + { + new BlockListLayoutItem { ContentKey = contentElementKey } + } + } + }, + ContentData = + [ + new() + { + Key = contentElementKey, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + Values = + [ + new () + { + Alias = "singleLineText", + Value = "The single line text" + } + ] + } + ], + Expose = + [ + new (contentElementKey, null, null) + ] + }; + var blocksPropertyValue = JsonSerializer.Serialize(blockListValue); + + var content = new ContentBuilder() + .WithContentType(blockListContentType) + .WithName("My Blocks") + .WithPropertyValues(new { blocks = blocksPropertyValue }) + .Build(); + ContentService.Save(content); + + elementType.Variations = ContentVariation.Culture; + elementType.PropertyTypes.First(pt => pt.Alias == "singleLineText").Variations = ContentVariation.Culture; + ContentTypeService.Save(elementType); + + var valueEditor = await GetValueEditor(blockListContentType); + var toEditorValue = valueEditor.ToEditor(content.Properties["blocks"]!) as BlockListValue; + Assert.IsNotNull(toEditorValue); + Assert.AreEqual(1, toEditorValue.ContentData.Count); + + var properties = toEditorValue.ContentData.First().Values; + Assert.AreEqual(1, properties.Count); + Assert.Multiple(() => + { + var property = properties.First(); + Assert.AreEqual("singleLineText", property.Alias); + Assert.AreEqual("The single line text", property.Value); + Assert.AreEqual("en-US", property.Culture); + }); + + Assert.AreEqual(1, toEditorValue.Expose.Count); + Assert.Multiple(() => + { + var itemVariation = toEditorValue.Expose[0]; + Assert.AreEqual(contentElementKey, itemVariation.ContentKey); + Assert.AreEqual("en-US", itemVariation.Culture); + }); + } + + [Test] + public async Task Can_Handle_Culture_Variance_Removal() + { + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + elementType.Variations = ContentVariation.Culture; + elementType.PropertyTypes.First(pt => pt.Alias == "singleLineText").Variations = ContentVariation.Culture; + ContentTypeService.Save(elementType); + + var blockListContentType = await CreateBlockListContentType(elementType); + + var contentElementKey = Guid.NewGuid(); + var blockListValue = new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockList, + new IBlockLayoutItem[] + { + new BlockListLayoutItem { ContentKey = contentElementKey } + } + } + }, + ContentData = + [ + new() + { + Key = contentElementKey, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + Values = + [ + new () + { + Alias = "singleLineText", + Value = "The single line text", + Culture = "en-US" + } + ] + } + ], + Expose = + [ + new (contentElementKey, "en-US", null) + ] + }; + var blocksPropertyValue = JsonSerializer.Serialize(blockListValue); + + var content = new ContentBuilder() + .WithContentType(blockListContentType) + .WithName("My Blocks") + .WithPropertyValues(new { blocks = blocksPropertyValue }) + .Build(); + ContentService.Save(content); + + elementType.PropertyTypes.First(pt => pt.Alias == "singleLineText").Variations = ContentVariation.Nothing; + elementType.Variations = ContentVariation.Nothing; + ContentTypeService.Save(elementType); + + var valueEditor = await GetValueEditor(blockListContentType); + var toEditorValue = valueEditor.ToEditor(content.Properties["blocks"]!) as BlockListValue; + Assert.IsNotNull(toEditorValue); + Assert.AreEqual(1, toEditorValue.ContentData.Count); + + var properties = toEditorValue.ContentData.First().Values; + Assert.AreEqual(1, properties.Count); + Assert.Multiple(() => + { + var property = properties.First(); + Assert.AreEqual("singleLineText", property.Alias); + Assert.AreEqual("The single line text", property.Value); + Assert.AreEqual(null, property.Culture); + }); + + Assert.AreEqual(1, toEditorValue.Expose.Count); + Assert.Multiple(() => + { + var itemVariation = toEditorValue.Expose[0]; + Assert.AreEqual(contentElementKey, itemVariation.ContentKey); + Assert.AreEqual(null, itemVariation.Culture); + }); + } + + private async Task CreateBlockListContentType(IContentType elementType) + { + var blockListDataType = new DataType(PropertyEditorCollection[Constants.PropertyEditors.Aliases.BlockList], ConfigurationEditorJsonSerializer) + { + ConfigurationData = new Dictionary + { + { + "blocks", + new BlockListConfiguration.BlockConfiguration[] + { + new() { ContentElementTypeKey = elementType.Key } + } + } + }, + Name = "My Block List", + DatabaseType = ValueStorageType.Ntext, + ParentId = Constants.System.Root, + CreateDate = DateTime.UtcNow + }; + + await DataTypeService.CreateAsync(blockListDataType, Constants.Security.SuperUserKey); + + var contentType = new ContentTypeBuilder() + .WithAlias("myPage") + .WithName("My Page") + .AddPropertyType() + .WithAlias("blocks") + .WithName("Blocks") + .WithDataTypeId(blockListDataType.Id) + .Done() + .Build(); + ContentTypeService.Save(contentType); + // re-fetch to wire up all key bindings (particularly to the datatype) + return await ContentTypeService.GetAsync(contentType.Key); + } + + private async Task GetValueEditor(IContentType contentType) + { + var dataType = await DataTypeService.GetAsync(contentType.PropertyTypes.First(propertyType => propertyType.Alias == "blocks").DataTypeKey); + Assert.IsNotNull(dataType?.Editor); + var valueEditor = dataType.Editor.GetValueEditor() as BlockListPropertyEditorBase.BlockListEditorPropertyValueEditor; + Assert.IsNotNull(valueEditor); + + return valueEditor; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/PropertyIndexValueFactoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/PropertyIndexValueFactoryTests.cs index f5a9d51bbf..ac386f10d2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/PropertyIndexValueFactoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/PropertyIndexValueFactoryTests.cs @@ -46,20 +46,22 @@ public class PropertyIndexValueFactoryTests : UmbracoIntegrationTest var propertyValue = RichTextPropertyEditorHelper.SerializeRichTextEditorValue( new RichTextEditorValue { - Markup = @$"

This is some markup

", + Markup = @$"

This is some markup

", Blocks = JsonSerializer.Deserialize($$""" { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/{{elementId:N}}" + "contentKey": "{{elementId:D}}" } ] }, "contentData": [{ "contentTypeKey": "{{elementType.Key:D}}", - "udi": "umb://element/{{elementId:N}}", - "singleLineText": "The single line of text in the block", - "bodyText": "

The body text in the block

" + "key": "{{elementId:D}}", + "values": [ + { "alias": "singleLineText", "value": "The single line of text in the block" }, + { "alias": "bodyText", "value": "

The body text in the block

" } + ] } ], "settingsData": [] @@ -170,24 +172,23 @@ public class PropertyIndexValueFactoryTests : UmbracoIntegrationTest var editor = dataType.Editor!; - var contentElementUdi = new GuidUdi(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementKey = Guid.NewGuid(); var blockListValue = new BlockListValue( [ - new BlockListLayoutItem(contentElementUdi) + new BlockListLayoutItem(contentElementKey) ]) { ContentData = [ - new(contentElementUdi, elementType.Key, elementType.Alias) + new(contentElementKey, elementType.Key, elementType.Alias) { - RawPropertyValues = new Dictionary + Values = new List { - {"singleLineText", "The single line of text in the block"}, - {"bodyText", "

The body text in the block

"} + new() { Alias = "singleLineText", Value = "The single line of text in the block" }, + new() { Alias = "bodyText", Value = "

The body text in the block

" }, } } ], - SettingsData = [] }; var propertyValue = JsonSerializer.Serialize(blockListValue); @@ -272,11 +273,11 @@ public class PropertyIndexValueFactoryTests : UmbracoIntegrationTest var editor = dataType.Editor!; - var contentElementUdi = new GuidUdi(Constants.UdiEntityType.Element, Guid.NewGuid()); - var contentAreaElementUdi = new GuidUdi(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementKey = Guid.NewGuid(); + var contentAreaElementKey = Guid.NewGuid(); var blockGridValue = new BlockGridValue( [ - new BlockGridLayoutItem(contentElementUdi) + new BlockGridLayoutItem(contentElementKey) { ColumnSpan = 12, RowSpan = 1, @@ -286,7 +287,7 @@ public class PropertyIndexValueFactoryTests : UmbracoIntegrationTest { Items = [ - new BlockGridLayoutItem(contentAreaElementUdi) + new BlockGridLayoutItem(contentAreaElementKey) { ColumnSpan = 12, RowSpan = 1, @@ -299,22 +300,22 @@ public class PropertyIndexValueFactoryTests : UmbracoIntegrationTest { ContentData = [ - new(contentElementUdi, elementType.Key, elementType.Alias) + new(contentElementKey, elementType.Key, elementType.Alias) { - RawPropertyValues = new() + Values = new List { - { "singleLineText", "The single line of text in the grid root" }, - { "bodyText", "

The body text in the grid root

" }, - }, + new() { Alias = "singleLineText", Value = "The single line of text in the grid root" }, + new() { Alias = "bodyText", Value = "

The body text in the grid root

" }, + } }, - new(contentAreaElementUdi, elementType.Key, elementType.Alias) + new(contentAreaElementKey, elementType.Key, elementType.Alias) { - RawPropertyValues = new() + Values = new List { - { "singleLineText", "The single line of text in the grid area" }, - { "bodyText", "

The body text in the grid area

" }, - }, - }, + new() { Alias = "singleLineText", Value = "The single line of text in the grid area" }, + new() { Alias = "bodyText", Value = "

The body text in the grid area

" }, + } + } ], }; var propertyValue = JsonSerializer.Serialize(blockGridValue); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs new file mode 100644 index 0000000000..934ceea083 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs @@ -0,0 +1,495 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.DeliveryApi; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Tests.Common.Builders; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +// NOTE: These tests are in place to ensure that element level variation works for Rich Text. Element level variation +// is tested more in-depth for Block List (see BlockListElementLevelVariationTests), but since the actual +// implementation is shared between Block List and Rich Text, we won't repeat all those tests here. +public class RichTextElementLevelVariationTests : BlockEditorElementVariationTestBase +{ + private IJsonSerializer JsonSerializer => GetRequiredService(); + + [Test] + public async Task Can_Publish_Cultures_Independently() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var blockGridDataType = await CreateRichTextDataType(elementType); + var contentType = CreateContentType(blockGridDataType); + var richTextValue = CreateRichTextValue(elementType); + var content = CreateContent(contentType, richTextValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValues( + "en-US", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant content value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The first content value in English", element1.Properties["variantText"]); + Assert.AreEqual("#2: The first invariant content value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The first content value in English", element2.Properties["variantText"]); + Assert.AreEqual("#3: The first invariant content value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The first content value in English", element3.Properties["variantText"]); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant settings value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The first settings value in English", element1.Properties["variantText"]); + Assert.AreEqual("#2: The first invariant settings value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The first settings value in English", element2.Properties["variantText"]); + Assert.AreEqual("#3: The first invariant settings value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The first settings value in English", element3.Properties["variantText"]); + }); + + AssertPropertyValues( + "da-DK", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant content value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The first content value in Danish", element1.Properties["variantText"]); + Assert.AreEqual("#2: The first invariant content value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The first content value in Danish", element2.Properties["variantText"]); + Assert.AreEqual("#3: The first invariant content value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The first content value in Danish", element3.Properties["variantText"]); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant settings value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The first settings value in Danish", element1.Properties["variantText"]); + Assert.AreEqual("#2: The first invariant settings value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The first settings value in Danish", element2.Properties["variantText"]); + Assert.AreEqual("#3: The first invariant settings value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The first settings value in Danish", element3.Properties["variantText"]); + }); + + richTextValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + for (var i = 0; i < 3; i++) + { + richTextValue.Blocks.ContentData[i].Values[0].Value = $"#{i + 1}: The second invariant content value"; + richTextValue.Blocks.ContentData[i].Values[1].Value = $"#{i + 1}: The second content value in English"; + richTextValue.Blocks.ContentData[i].Values[2].Value = $"#{i + 1}: The second content value in Danish"; + richTextValue.Blocks.SettingsData[i].Values[0].Value = $"#{i + 1}: The second invariant settings value"; + richTextValue.Blocks.SettingsData[i].Values[1].Value = $"#{i + 1}: The second settings value in English"; + richTextValue.Blocks.SettingsData[i].Values[2].Value = $"#{i + 1}: The second settings value in Danish"; + } + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(richTextValue)); + ContentService.Save(content); + PublishContent(content, ["en-US"]); + + AssertPropertyValues( + "en-US", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant content value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The second content value in English", element1.Properties["variantText"]); + Assert.AreEqual("#2: The second invariant content value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The second content value in English", element2.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant content value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The second content value in English", element3.Properties["variantText"]); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant settings value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The second settings value in English", element1.Properties["variantText"]); + Assert.AreEqual("#2: The second invariant settings value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The second settings value in English", element2.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant settings value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The second settings value in English", element3.Properties["variantText"]); + }); + + AssertPropertyValues( + "da-DK", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant content value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The first content value in Danish", element1.Properties["variantText"]); + Assert.AreEqual("#2: The second invariant content value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The first content value in Danish", element2.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant content value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The first content value in Danish", element3.Properties["variantText"]); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant settings value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The first settings value in Danish", element1.Properties["variantText"]); + Assert.AreEqual("#2: The second invariant settings value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The first settings value in Danish", element2.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant settings value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The first settings value in Danish", element3.Properties["variantText"]); + }); + + PublishContent(content, ["da-DK"]); + + AssertPropertyValues( + "da-DK", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant content value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The second content value in Danish", element1.Properties["variantText"]); + Assert.AreEqual("#2: The second invariant content value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The second content value in Danish", element2.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant content value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The second content value in Danish", element3.Properties["variantText"]); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant settings value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The second settings value in Danish", element1.Properties["variantText"]); + Assert.AreEqual("#2: The second invariant settings value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The second settings value in Danish", element2.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant settings value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The second settings value in Danish", element3.Properties["variantText"]); + }); + + void AssertPropertyValues( + string culture, + Action validateBlockContentValues, + Action validateBlockSettingsValues) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + var property = publishedContent.GetProperty("blocks"); + Assert.IsNotNull(property); + + var propertyValue = property.GetDeliveryApiValue(false, culture) as RichTextModel; + Assert.IsNotNull(propertyValue); + + var blocks = propertyValue.Blocks.ToArray(); + Assert.AreEqual(3, blocks.Length); + + Assert.Multiple(() => + { + validateBlockContentValues(blocks[0].Content, blocks[1].Content, blocks[2].Content); + validateBlockSettingsValues(blocks[0].Settings, blocks[1].Settings, blocks[2].Settings); + }); + } + } + + [Test] + public async Task Can_Publish_With_Blocks_Removed() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var blockGridDataType = await CreateRichTextDataType(elementType); + var contentType = CreateContentType(blockGridDataType); + var richTextValue = CreateRichTextValue(elementType); + var content = CreateContent(contentType, richTextValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 3, blocks => { }); + AssertPropertyValues("da-DK", 3, blocks => { }); + + richTextValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + + // remove block #2 + var richTextBlockLayout = richTextValue.Blocks!.Layout.First(); + richTextValue.Blocks.Layout[richTextBlockLayout.Key] = + [ + richTextBlockLayout.Value.First(), + richTextBlockLayout.Value.Last() + ]; + var contentKey = richTextValue.Blocks.ContentData[1].Key; + richTextValue.Blocks.ContentData.RemoveAt(1); + richTextValue.Blocks.SettingsData.RemoveAt(1); + richTextValue.Blocks.Expose.RemoveAll(v => v.ContentKey == contentKey); + Assert.AreEqual(4, richTextValue.Blocks.Expose.Count); + + richTextValue.Blocks.ContentData[0].Values[0].Value = "#1: The second invariant content value"; + richTextValue.Blocks.ContentData[0].Values[1].Value = "#1: The second content value in English"; + richTextValue.Blocks.ContentData[0].Values[2].Value = "#1: The second content value in Danish"; + richTextValue.Blocks.ContentData[1].Values[0].Value = "#3: The second invariant content value"; + richTextValue.Blocks.ContentData[1].Values[1].Value = "#3: The second content value in English"; + richTextValue.Blocks.ContentData[1].Values[2].Value = "#3: The second content value in Danish"; + richTextValue.Blocks.SettingsData[0].Values[0].Value = "#1: The second invariant settings value"; + richTextValue.Blocks.SettingsData[0].Values[1].Value = "#1: The second settings value in English"; + richTextValue.Blocks.SettingsData[0].Values[2].Value = "#1: The second settings value in Danish"; + richTextValue.Blocks.SettingsData[1].Values[0].Value = "#3: The second invariant settings value"; + richTextValue.Blocks.SettingsData[1].Values[1].Value = "#3: The second settings value in English"; + richTextValue.Blocks.SettingsData[1].Values[2].Value = "#3: The second settings value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(richTextValue)); + ContentService.Save(content); + PublishContent(content, ["en-US"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Properties["invariantText"]); + Assert.AreEqual("#1: The second content value in English", blocks[0].Content.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Properties["invariantText"]); + Assert.AreEqual("#3: The second content value in English", blocks[1].Content.Properties["variantText"]); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Properties["invariantText"]); + Assert.AreEqual("#1: The second settings value in English", blocks[0].Settings.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant settings value", blocks[1].Settings!.Properties["invariantText"]); + Assert.AreEqual("#3: The second settings value in English", blocks[1].Settings.Properties["variantText"]); + }); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Properties["invariantText"]); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Properties["invariantText"]); + Assert.AreEqual("#3: The first content value in Danish", blocks[1].Content.Properties["variantText"]); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Properties["invariantText"]); + Assert.AreEqual("#1: The first settings value in Danish", blocks[0].Settings.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant settings value", blocks[1].Settings!.Properties["invariantText"]); + Assert.AreEqual("#3: The first settings value in Danish", blocks[1].Settings.Properties["variantText"]); + }); + + PublishContent(content, ["da-DK"]); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Properties["invariantText"]); + Assert.AreEqual("#1: The second content value in Danish", blocks[0].Content.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Properties["invariantText"]); + Assert.AreEqual("#3: The second content value in Danish", blocks[1].Content.Properties["variantText"]); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Properties["invariantText"]); + Assert.AreEqual("#1: The second settings value in Danish", blocks[0].Settings.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant settings value", blocks[1].Settings!.Properties["invariantText"]); + Assert.AreEqual("#3: The second settings value in Danish", blocks[1].Settings.Properties["variantText"]); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + var property = publishedContent.GetProperty("blocks"); + Assert.IsNotNull(property); + + var propertyValue = property.GetDeliveryApiValue(false, culture) as RichTextModel; + Assert.IsNotNull(propertyValue); + + var blocks = propertyValue.Blocks.ToArray(); + Assert.AreEqual(numberOfExpectedBlocks, blocks.Length); + + validateBlocks(blocks); + } + } + + [Test] + public async Task Markup_Follows_Invariance() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var blockGridDataType = await CreateRichTextDataType(elementType); + var contentType = CreateContentType(blockGridDataType); + var richTextValue = CreateRichTextValue(elementType); + var content = CreateContent(contentType, richTextValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValuesForAllCultures(markup => + { + Assert.Multiple(() => + { + Assert.IsTrue(markup.Contains("

Some text.

")); + Assert.IsTrue(markup.Contains("

More text.

")); + Assert.IsTrue(markup.Contains("

Even more text.

")); + Assert.IsTrue(markup.Contains("

The end.

")); + }); + }); + + richTextValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + richTextValue.Markup = richTextValue.Markup + .Replace("Some text", "Some text updated") + .Replace("More text", "More text updated") + .Replace("Even more text", "Even more text updated") + .Replace("The end", "The end updated"); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(richTextValue)); + ContentService.Save(content); + PublishContent(content, ["en-US"]); + + AssertPropertyValuesForAllCultures(markup => + { + Assert.Multiple(() => + { + Assert.IsFalse(markup.Contains("

Some text.

")); + Assert.IsFalse(markup.Contains("

More text.

")); + Assert.IsFalse(markup.Contains("

Even more text.

")); + Assert.IsFalse(markup.Contains("

The end.

")); + Assert.IsTrue(markup.Contains("

Some text updated.

")); + Assert.IsTrue(markup.Contains("

More text updated.

")); + Assert.IsTrue(markup.Contains("

Even more text updated.

")); + Assert.IsTrue(markup.Contains("

The end updated.

")); + }); + }); + + PublishContent(content, ["da-DK"]); + + AssertPropertyValuesForAllCultures(markup => + { + Assert.Multiple(() => + { + Assert.IsFalse(markup.Contains("

Some text.

")); + Assert.IsFalse(markup.Contains("

More text.

")); + Assert.IsFalse(markup.Contains("

Even more text.

")); + Assert.IsFalse(markup.Contains("

The end.

")); + Assert.IsTrue(markup.Contains("

Some text updated.

")); + Assert.IsTrue(markup.Contains("

More text updated.

")); + Assert.IsTrue(markup.Contains("

Even more text updated.

")); + Assert.IsTrue(markup.Contains("

The end updated.

")); + }); + }); + + void AssertPropertyValuesForAllCultures(Action validateMarkup) + { + foreach (var culture in new[] { "en-US", "da-DK" }) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + var property = publishedContent.GetProperty("blocks"); + Assert.IsNotNull(property); + + var propertyValue = property.GetDeliveryApiValue(false, culture) as RichTextModel; + Assert.IsNotNull(propertyValue); + + Assert.IsNotEmpty(propertyValue.Markup); + validateMarkup(propertyValue.Markup); + } + } + } + + private async Task CreateRichTextDataType(IContentType elementType) + => await CreateBlockEditorDataType( + Constants.PropertyEditors.Aliases.RichText, + new RichTextConfiguration.RichTextBlockConfiguration[] + { + new() + { + ContentElementTypeKey = elementType.Key, + SettingsElementTypeKey = elementType.Key, + } + }); + + private IContentType CreateContentType(IDataType blockListDataType) + => CreateContentType(ContentVariation.Culture, blockListDataType); + + private RichTextEditorValue CreateRichTextValue(IContentType elementType) + { + var contentElementKey1 = Guid.NewGuid(); + var settingsElementKey1 = Guid.NewGuid(); + var contentElementKey2 = Guid.NewGuid(); + var settingsElementKey2 = Guid.NewGuid(); + var contentElementKey3 = Guid.NewGuid(); + var settingsElementKey3 = Guid.NewGuid(); + + return new RichTextEditorValue + { + Markup = $""" +

Some text.

+ +

More text.

+ +

Even more text.

+ +

The end.

+ """, + Blocks = new RichTextBlockValue([ + new RichTextBlockLayoutItem(contentElementKey1, settingsElementKey1), + new RichTextBlockLayoutItem(contentElementKey2, settingsElementKey2), + new RichTextBlockLayoutItem(contentElementKey3, settingsElementKey3), + ]) + { + ContentData = + [ + new(contentElementKey1, elementType.Key, elementType.Alias) + { + Values = + [ + new() { Alias = "invariantText", Value = "#1: The first invariant content value" }, + new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" } + ] + }, + new(contentElementKey2, elementType.Key, elementType.Alias) + { + Values = + [ + new() { Alias = "invariantText", Value = "#2: The first invariant content value" }, + new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" } + ] + }, + new(contentElementKey3, elementType.Key, elementType.Alias) + { + Values = + [ + new() { Alias = "invariantText", Value = "#3: The first invariant content value" }, + new() { Alias = "variantText", Value = "#3: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#3: The first content value in Danish", Culture = "da-DK" } + ] + }, + ], + SettingsData = + [ + new(settingsElementKey1, elementType.Key, elementType.Alias) + { + Values = + [ + new() { Alias = "invariantText", Value = "#1: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" } + ] + }, + new(settingsElementKey2, elementType.Key, elementType.Alias) + { + Values = + [ + new() { Alias = "invariantText", Value = "#2: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" } + ] + }, + new(settingsElementKey3, elementType.Key, elementType.Alias) + { + Values = + [ + new() { Alias = "invariantText", Value = "#3: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#3: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#3: The first settings value in Danish", Culture = "da-DK" } + ] + }, + ], + Expose = + [ + new (contentElementKey1, "en-US", null), + new (contentElementKey1, "da-DK", null), + new (contentElementKey2, "en-US", null), + new (contentElementKey2, "da-DK", null), + new (contentElementKey3, "en-US", null), + new (contentElementKey3, "da-DK", null), + ] + } + }; + } + + private IContent CreateContent(IContentType contentType, RichTextEditorValue richTextValue) + { + var contentBuilder = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName("en-US", "Home (en)") + .WithCultureName("da-DK", "Home (da)"); + + var content = contentBuilder.Build(); + + var propertyValue = JsonSerializer.Serialize(richTextValue); + content.Properties["blocks"]!.SetValue(propertyValue); + + ContentService.Save(content); + return content; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorTests.cs index 84717ac203..7e93230765 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorTests.cs @@ -94,19 +94,21 @@ public class RichTextPropertyEditorTests : UmbracoIntegrationTest var propertyValue = RichTextPropertyEditorHelper.SerializeRichTextEditorValue( new RichTextEditorValue { - Markup = @$"

This is some markup

", + Markup = @$"

This is some markup

", Blocks = JsonSerializer.Deserialize($$""" { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/{{elementId:N}}" + "contentKey": "{{elementId:D}}" } ] }, "contentData": [{ "contentTypeKey": "{{elementType.Key:D}}", - "udi": "umb://element/{{elementId:N}}", - "contentPicker": "umb://document/{{pickedContent.Key:N}}" + "key": "{{elementId:D}}", + "values": [ + { "alias": "contentPicker", "value": "umb://document/{{pickedContent.Key:N}}" } + ] } ], "settingsData": [] @@ -145,19 +147,21 @@ public class RichTextPropertyEditorTests : UmbracoIntegrationTest var propertyValue = RichTextPropertyEditorHelper.SerializeRichTextEditorValue( new RichTextEditorValue { - Markup = @$"

This is some markup

", + Markup = @$"

This is some markup

", Blocks = JsonSerializer.Deserialize($$""" { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/{{elementId:N}}" + "contentKey": "{{elementId:D}}" } ] }, "contentData": [{ "contentTypeKey": "{{elementType.Key:D}}", - "udi": "umb://element/{{elementId:N}}", - "tags": "[\"Tag One\", \"Tag Two\", \"Tag Three\"]" + "key": "{{elementId:D}}", + "values": [ + { "alias": "tags", "value": "[\"Tag One\", \"Tag Two\", \"Tag Three\"]" } + ] } ], "settingsData": [] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs index ef8d46a071..00e8284135 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs @@ -42,64 +42,93 @@ public class ContentValidationServiceTests : UmbracoIntegrationTestWithContent { "layout": { "Umbraco.BlockList": [{ - "contentUdi": "umb://element/9addc377c02c4db088c273b933704f7b", - "settingsUdi": "umb://element/65db1ecd78e041a584f07296123a0a73" + "contentKey": "9addc377-c02c-4db0-88c2-73b933704f7b", + "settingsKey": "65db1ecd-78e0-41a5-84f0-7296123a0a73" }, { - "contentUdi": "umb://element/3af93b5b5e404c64b1422564309fc4c7", - "settingsUdi": "umb://element/efb9583ce67043f282fb2a0cb0f3e736" + "contentKey": "3af93b5b-5e40-4c64-b142-2564309fc4c7", + "settingsKey": "efb9583c-e670-43f2-82fb-2a0cb0f3e736" } ] }, "contentData": [{ "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/9addc377c02c4db088c273b933704f7b", - "title": "Valid root content", - "blocks": { - "layout": { - "Umbraco.BlockList": [{ - "contentUdi": "umb://element/f36cebfad03b44519e604bf32c5b1e2f", - "settingsUdi": "umb://element/c9129a4671bb4b4e8f0ad525ad4a5de3" - }, { - "contentUdi": "umb://element/b8173e4a0618475c8277c3c6af68bee6", - "settingsUdi": "umb://element/77f7ea3507664395bf7f0c9df04530f7" - } - ] - }, - "contentData": [{ - "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/f36cebfad03b44519e604bf32c5b1e2f", - "title": "Invalid nested content" - }, { - "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/b8173e4a0618475c8277c3c6af68bee6", - "title": "Valid nested content" + "key": "9addc377-c02c-4db0-88c2-73b933704f7b", + "values": [ + { + "alias": "title", + "value": "Valid root content title" + }, + { + "alias": "blocks", + "value": { + "layout": { + "Umbraco.BlockList": [{ + "contentKey": "f36cebfa-d03b-4451-9e60-4bf32c5b1e2f", + "settingsKey": "c9129a46-71bb-4b4e-8f0a-d525ad4a5de3" + }, { + "contentKey": "b8173e4a-0618-475c-8277-c3c6af68bee6", + "settingsKey": "77f7ea35-0766-4395-bf7f-0c9df04530f7" + } + ] + }, + "contentData": [{ + "contentTypeKey": "{{setup.ElementType.Key}}", + "key": "f36cebfa-d03b-4451-9e60-4bf32c5b1e2f", + "values": [ + { "alias": "title", "value": "Invalid nested content title (ref #4)" }, + { "alias": "text", "value": "Valid nested content text" } + ] + }, { + "contentTypeKey": "{{setup.ElementType.Key}}", + "key": "b8173e4a-0618-475c-8277-c3c6af68bee6", + "values": [ + { "alias": "title", "value": "Valid nested content title" }, + { "alias": "text", "value": "Invalid nested content text (ref #5)" } + ] + } + ], + "settingsData": [{ + "contentTypeKey": "{{setup.ElementType.Key}}", + "key": "c9129a46-71bb-4b4e-8f0a-d525ad4a5de3", + "values": [ + { "alias": "title", "value": "Valid nested setting title" }, + { "alias": "text", "value": "Invalid nested setting text (ref #6)" } + ] + }, { + "contentTypeKey": "{{setup.ElementType.Key}}", + "key": "77f7ea35-0766-4395-bf7f-0c9df04530f7", + "values": [ + { "alias": "title", "value": "Invalid nested setting title (ref #7)" }, + { "alias": "text", "value": "Valid nested setting text)" } + ] + } + ] + } } - ], - "settingsData": [{ - "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/c9129a4671bb4b4e8f0ad525ad4a5de3", - "title": "Valid nested setting" - }, { - "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/77f7ea3507664395bf7f0c9df04530f7", - "title": "Invalid nested setting" - } - ] - } + ] }, { "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/3af93b5b5e404c64b1422564309fc4c7", - "title": "Invalid root content" + "key": "3af93b5b-5e40-4c64-b142-2564309fc4c7", + "values": [ + { "alias": "title", "value": "Invalid root content title (ref #1)" }, + { "alias": "text", "value": "Valid root content text" } + ] } ], "settingsData": [{ "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/65db1ecd78e041a584f07296123a0a73", - "title": "Invalid root setting" + "key": "65db1ecd-78e0-41a5-84f0-7296123a0a73", + "values": [ + { "alias": "title", "value": "Invalid root setting title (ref #2)" }, + { "alias": "text", "value": "Valid root setting text" } + ] }, { "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/efb9583ce67043f282fb2a0cb0f3e736", - "title": "Valid root setting" + "key": "efb9583c-e670-43f2-82fb-2a0cb0f3e736", + "values": [ + { "alias": "title", "value": "Valid root setting title" }, + { "alias": "text", "value": "Invalid root setting text (ref #3)" } + ] } ] } @@ -109,11 +138,23 @@ public class ContentValidationServiceTests : UmbracoIntegrationTestWithContent }, setup.DocumentType); - Assert.AreEqual(4, validationResult.ValidationErrors.Count()); - Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].blocks.contentData[0].title")); - Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].blocks.settingsData[1].title")); - Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[1].title")); - Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".settingsData[0].title")); + Assert.AreEqual(7, validationResult.ValidationErrors.Count()); + + // ref #1 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[1].values[0].value")); + // ref #2 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".settingsData[0].values[0].value")); + // ref #3 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".settingsData[1].values[1].value")); + + // ref #4 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].values[1].value.contentData[0].values[0].value")); + // ref #5 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].values[1].value.contentData[1].values[1].value")); + // ref #6 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].values[1].value.settingsData[0].values[1].value")); + // ref #7 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].values[1].value.settingsData[1].values[0].value")); } [TestCase(true)] @@ -461,6 +502,10 @@ public class ContentValidationServiceTests : UmbracoIntegrationTestWithContent { ValidationRegExp = "^Valid.*$" }); + elementType.AddPropertyType(new PropertyType(ShortStringHelper, textBoxDataType, "text") + { + ValidationRegExp = "^Valid.*$" + }); await ContentTypeService.SaveAsync(elementType, Constants.Security.SuperUserKey); // create a document type with the block list and a regex validated text box diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 6dfb33f112..7f930c6144 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -158,6 +158,12 @@ MediaTypeEditingServiceTests.cs + + BlockListElementLevelVariationTests.cs + + + BlockListElementLevelVariationTests.cs + DocumentNavigationServiceTests.cs diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs index a2522a5ecd..88f76dc7f3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs @@ -259,7 +259,7 @@ public class RichTextParserTests : PropertyValueConverterTests var id = Guid.NewGuid(); var tagName = $"umb-rte-block{(inlineBlock ? "-inline" : string.Empty)}"; - var element = parser.Parse($"

<{tagName} data-content-udi=\"umb://element/{id:N}\">

") as RichTextRootElement; + var element = parser.Parse($"

<{tagName} data-content-key=\"{id:N}\">

") as RichTextRootElement; Assert.IsNotNull(element); var paragraph = element.Elements.Single() as RichTextGenericElement; Assert.IsNotNull(paragraph); @@ -296,7 +296,7 @@ public class RichTextParserTests : PropertyValueConverterTests }); var tagName = $"umb-rte-block{(inlineBlock ? "-inline" : string.Empty)}"; - var element = parser.Parse($"

<{tagName} data-content-udi=\"umb://element/{block1ContentId:N}\"><{tagName} data-content-udi=\"umb://element/{block2ContentId:N}\">

", richTextBlockModel) as RichTextRootElement; + var element = parser.Parse($"

<{tagName} data-content-key=\"{block1ContentId:N}\"><{tagName} data-content-key=\"{block2ContentId:N}\">

", richTextBlockModel) as RichTextRootElement; Assert.IsNotNull(element); var paragraph = element.Elements.Single() as RichTextGenericElement; Assert.IsNotNull(paragraph); @@ -333,7 +333,7 @@ public class RichTextParserTests : PropertyValueConverterTests var id1 = Guid.NewGuid(); var id2 = Guid.NewGuid(); - var element = parser.Parse($"

") as RichTextRootElement; + var element = parser.Parse($"

") as RichTextRootElement; Assert.IsNotNull(element); Assert.AreEqual(2, element.Elements.Count()); @@ -435,7 +435,7 @@ public class RichTextParserTests : PropertyValueConverterTests var id = Guid.NewGuid(); var tagName = $"umb-rte-block{(inlineBlock ? "-inline" : string.Empty)}"; - var result = parser.Parse($"

<{tagName} data-content-udi=\"umb://element/{id:N}\">

"); + var result = parser.Parse($"

<{tagName} data-content-key=\"{id:N}\">

"); Assert.AreEqual($"

<{tagName} data-content-id=\"{id:D}\">

", result); } @@ -446,7 +446,7 @@ public class RichTextParserTests : PropertyValueConverterTests var id1 = Guid.NewGuid(); var id2 = Guid.NewGuid(); - var result = parser.Parse($"

"); + var result = parser.Parse($"

"); Assert.AreEqual($"

", result); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs index b918613820..d7a5e372a2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -26,6 +27,8 @@ public class ContentTests { private readonly IContentTypeService _contentTypeService = Mock.Of(); + private readonly PropertyEditorCollection _propertyEditorCollection = new (new DataEditorCollection(() => [])); + [Test] public void Variant_Culture_Names_Track_Dirty_Changes() { @@ -87,7 +90,7 @@ public class ContentTests Thread.Sleep(500); // The "Date" wont be dirty if the test runs too fast since it will be the same date content.SetCultureName("name-fr", langFr); - content.PublishCulture(CultureImpact.Explicit(langFr, false)); // we've set the name, now we're publishing it + content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection); // we've set the name, now we're publishing it Assert.IsTrue( content.IsPropertyDirty("PublishCultureInfos")); // now it will be changed since the collection has changed var frCultureName = content.PublishCultureInfos[langFr]; @@ -100,7 +103,7 @@ public class ContentTests Thread.Sleep(500); // The "Date" wont be dirty if the test runs too fast since it will be the same date content.SetCultureName("name-fr", langFr); - content.PublishCulture(CultureImpact.Explicit(langFr, false)); // we've set the name, now we're publishing it + content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection); // we've set the name, now we're publishing it Assert.IsTrue(frCultureName.IsPropertyDirty("Date")); Assert.IsTrue(content.IsPropertyDirty("PublishCultureInfos")); // it's true now since we've updated a name } @@ -300,7 +303,7 @@ public class ContentTests content.SetCultureName("Hello", "en-US"); content.SetCultureName("World", "es-ES"); - content.PublishCulture(CultureImpact.All); + content.PublishCulture(CultureImpact.All, DateTime.Now, _propertyEditorCollection); // should not try to clone something that's not Published or Unpublished // (and in fact it will not work) @@ -413,7 +416,7 @@ public class ContentTests content.SetCultureName("Hello", "en-US"); content.SetCultureName("World", "es-ES"); - content.PublishCulture(CultureImpact.All); + content.PublishCulture(CultureImpact.All, DateTime.Now, _propertyEditorCollection); var i = 200; foreach (var property in content.Properties) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs index c68f3f5b65..a9ff943921 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs @@ -22,6 +22,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models; [TestFixture] public class VariationTests { + private readonly PropertyEditorCollection _propertyEditorCollection = new (new DataEditorCollection(() => [])); + [Test] public void ValidateVariationTests() { @@ -315,7 +317,7 @@ public class VariationTests // can publish value // and get edited and published values - Assert.IsTrue(content.PublishCulture(CultureImpact.All)); + Assert.IsTrue(content.PublishCulture(CultureImpact.All, DateTime.Now, _propertyEditorCollection)); Assert.AreEqual("a", content.GetValue("prop")); Assert.AreEqual("a", content.GetValue("prop", published: true)); @@ -345,9 +347,9 @@ public class VariationTests // can publish value // and get edited and published values - Assert.IsFalse(content.PublishCulture(CultureImpact.Explicit(langFr, false))); // no name + Assert.IsFalse(content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection)); // no name content.SetCultureName("name-fr", langFr); - Assert.IsTrue(content.PublishCulture(CultureImpact.Explicit(langFr, false))); + Assert.IsTrue(content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection)); Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); @@ -361,7 +363,7 @@ public class VariationTests Assert.IsNull(content.GetValue("prop", langFr, published: true)); // can publish all - Assert.IsTrue(content.PublishCulture(CultureImpact.All)); + Assert.IsTrue(content.PublishCulture(CultureImpact.All, DateTime.Now, _propertyEditorCollection)); Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); @@ -371,14 +373,14 @@ public class VariationTests content.UnpublishCulture(langFr); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.IsNull(content.GetValue("prop", langFr, published: true)); - Assert.IsTrue(content.PublishCulture(CultureImpact.Explicit(langFr, false))); + Assert.IsTrue(content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); content.UnpublishCulture(); // clears invariant props if any Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); - Assert.IsTrue(content.PublishCulture(CultureImpact.All)); // publishes invariant props if any + Assert.IsTrue(content.PublishCulture(CultureImpact.All, DateTime.Now, _propertyEditorCollection)); // publishes invariant props if any Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); @@ -437,19 +439,19 @@ public class VariationTests var langFrImpact = CultureImpact.Explicit(langFr, true); Assert.IsTrue( - content.PublishCulture(langFrImpact)); // succeeds because names are ok (not validating properties here) + content.PublishCulture(langFrImpact, DateTime.Now, _propertyEditorCollection)); // succeeds because names are ok (not validating properties here) Assert.IsFalse( propertyValidationService.IsPropertyDataValid(content, out _, langFrImpact)); // fails because prop1 is mandatory content.SetValue("prop1", "a", langFr); Assert.IsTrue( - content.PublishCulture(langFrImpact)); // succeeds because names are ok (not validating properties here) + content.PublishCulture(langFrImpact, DateTime.Now, _propertyEditorCollection)); // succeeds because names are ok (not validating properties here) // Fails because prop2 is mandatory and invariant and the item isn't published. // Invariant is validated against the default language except when there isn't a published version, in that case it's always validated. Assert.IsFalse(propertyValidationService.IsPropertyDataValid(content, out _, langFrImpact)); content.SetValue("prop2", "x"); - Assert.IsTrue(content.PublishCulture(langFrImpact)); // still ok... + Assert.IsTrue(content.PublishCulture(langFrImpact, DateTime.Now, _propertyEditorCollection)); // still ok... Assert.IsTrue(propertyValidationService.IsPropertyDataValid(content, out _, langFrImpact)); // now it's ok Assert.AreEqual("a", content.GetValue("prop1", langFr, published: true)); @@ -485,12 +487,12 @@ public class VariationTests content.SetValue("prop", "a-es", langEs); // cannot publish without a name - Assert.IsFalse(content.PublishCulture(CultureImpact.Explicit(langFr, false))); + Assert.IsFalse(content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection)); // works with a name // and then FR is available, and published content.SetCultureName("name-fr", langFr); - Assert.IsTrue(content.PublishCulture(CultureImpact.Explicit(langFr, false))); + Assert.IsTrue(content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection)); // now UK is available too content.SetCultureName("name-uk", langUk); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs index 607a39a7f0..719c0885c8 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; @@ -16,12 +15,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] public class BlockEditorComponentTests { - private const string ContentGuid1 = "036ce82586a64dfba2d523a99ed80f58"; - private const string ContentGuid2 = "48288c21a38a40ef82deb3eda90a58f6"; - private const string SettingsGuid1 = "ffd35c4e2eea4900abfa5611b67b2492"; - private const string SubContentGuid1 = "4c44ce6b3a5c4f5f8f15e3dc24819a9e"; - private const string SubContentGuid2 = "a062c06d6b0b44ac892b35d90309c7f8"; - private const string SubSettingsGuid1 = "4d998d980ffa4eee8afdc23c4abd6d29"; + private const string ContentGuid1 = "709b857e-6f00-45c6-bf65-f7da028c361f"; + private const string ContentGuid2 = "823dc755-28ec-4198-b050-514d91b7994e"; + private const string SettingsGuid1 = "4d2e18fe-f030-4ea9-aed9-10e7aee265fd"; + private const string SubContentGuid1 = "b5698cf9-bf26-4c1c-8b1c-db30a1b5c56a"; + private const string SubContentGuid2 = "68606a64-a03a-4b78-bcb1-39daee0c590d"; + private const string SubSettingsGuid1 = "5ce1b7da-7c9f-491e-9b95-5510fd28c50c"; private readonly IJsonSerializer _jsonSerializer = new SystemTextJsonSerializer(); @@ -30,7 +29,7 @@ public class BlockEditorComponentTests { var component = new BlockListPropertyNotificationHandler(Mock.Of>()); var json = GetBlockListJson(null, string.Empty); - Assert.Throws(() => component.ReplaceBlockEditorUdis(json)); + Assert.Throws(() => component.ReplaceBlockEditorKeys(json)); } [Test] @@ -46,7 +45,7 @@ public class BlockEditorComponentTests var json = GetBlockListJson(null); var component = new BlockListPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); Assert.AreEqual(3, guidMap.Count); var expected = ReplaceGuids(json, guidMap); @@ -76,7 +75,7 @@ public class BlockEditorComponentTests var json = GetBlockListJson(innerJsonEscaped); var component = new BlockListPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); // the expected result is that the subFeatures data remains escaped Assert.AreEqual(6, guidMap.Count); @@ -105,7 +104,7 @@ public class BlockEditorComponentTests var json = GetBlockListJson(innerJson); var component = new BlockListPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); Assert.AreEqual(6, guidMap.Count); var expected = ReplaceGuids(GetBlockListJson(innerJson), guidMap); @@ -137,7 +136,7 @@ public class BlockEditorComponentTests var json = GetBlockListJson(complexEditorJsonEscaped); var component = new BlockListPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); // the expected result is that the subFeatures remains escaped Assert.AreEqual(6, guidMap.Count); @@ -168,7 +167,7 @@ public class BlockEditorComponentTests var json = GetBlockGridJson(innerJsonEscaped); var component = new BlockGridPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); // the expected result is that the subFeatures remains escaped Assert.AreEqual(13, guidMap.Count); @@ -195,7 +194,7 @@ public class BlockEditorComponentTests var json = GetBlockGridJson(innerJson); var component = new BlockGridPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); // the expected result is that the subFeatures remains unescaped Assert.AreEqual(13, guidMap.Count); @@ -228,7 +227,7 @@ public class BlockEditorComponentTests var json = GetBlockGridJson(innerJson); var component = new BlockGridPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); // the expected result is that the subFeatures remains unaltered - the UDIs within should still exist Assert.AreEqual(10, guidMap.Count); @@ -253,39 +252,41 @@ public class BlockEditorComponentTests { ""Umbraco.BlockList"": [ { - ""contentUdi"": """ + (contentGuid1.IsNullOrWhiteSpace() - ? string.Empty - : Udi.Create(Constants.UdiEntityType.Element, Guid.Parse(contentGuid1)).ToString()) + @""" + ""contentKey"": """ + (contentGuid1.IsNullOrWhiteSpace() ? string.Empty : contentGuid1) + @""" }, { - ""contentUdi"": ""umb://element/" + contentGuid2 + @""", - ""settingsUdi"": ""umb://element/" + settingsGuid1 + @""" + ""contentKey"": """ + contentGuid2 + @""", + ""settingsKey"": """ + settingsGuid1 + @""" } ] }, ""contentData"": [ { ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", - ""udi"": """ + (contentGuid1.IsNullOrWhiteSpace() - ? string.Empty - : Udi.Create(Constants.UdiEntityType.Element, Guid.Parse(contentGuid1)).ToString()) + @""", - ""featureName"": ""Hello"", - ""featureDetails"": ""World"" + ""key"": """ + (contentGuid1.IsNullOrWhiteSpace() ? string.Empty : contentGuid1) + @""", + ""values"": [ + { ""alias"": ""featureName"", ""value"": ""Hello"" }, + { ""alias"": ""featureDetails"", ""value"": ""World"" } + ] }, { ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", - ""udi"": ""umb://element/" + contentGuid2 + @""", - ""featureName"": ""Another"", - ""featureDetails"": ""Feature""" + - (subFeatures == null ? string.Empty : @", ""subFeatures"": " + subFeatures) + @" + ""key"": """ + contentGuid2 + @""", + ""values"": [ + { ""alias"": ""featureName"", ""value"": ""Another"" }, + { ""alias"": ""featureDetails"", ""value"": ""Feature"" }, + { ""alias"": ""subFeatures"", ""value"": " + subFeatures.OrIfNullOrWhiteSpace(@"""""") + @" } + ] } ], ""settingsData"": [ { ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", - ""udi"": ""umb://element/" + settingsGuid1 + @""", - ""featureName"": ""Setting 1"", - ""featureDetails"": ""Setting 2"" + ""key"": """ + settingsGuid1 + @""", + ""values"": [ + { ""alias"": ""featureName"", ""value"": ""Setting 1"" }, + { ""alias"": ""featureDetails"", ""value"": ""Setting 2"" } + ] } ] }"; @@ -350,7 +351,7 @@ public class BlockEditorComponentTests @"{ ""layout"": { ""Umbraco.BlockGrid"": [{ - ""contentUdi"": ""umb://element/d05861169d124582a7c2826e52a51b47"", + ""contentKey"": ""fb0595b1-26e7-493f-86c7-bf2c42326850"", ""areas"": [{ ""key"": ""b17663f0-c1f4-4bee-97cd-290fbc7b9a2c"", ""items"": [] @@ -361,13 +362,13 @@ public class BlockEditorComponentTests ], ""columnSpan"": 12, ""rowSpan"": 1, - ""settingsUdi"": ""umb://element/262d5efd2eeb43ed95e95c094c45ce1c"" + ""settingsKey"": ""0183ae81-2b62-49b5-8ac6-88d66c33068c"" }, { - ""contentUdi"": ""umb://element/5abad9f1b4e24d7aa269fbd1b50033ac"", + ""contentKey"": ""4852d9ef-ac8d-4d44-87c9-82d282aa0e7f"", ""areas"": [{ ""key"": ""b17663f0-c1f4-4bee-97cd-290fbc7b9a2c"", ""items"": [{ - ""contentUdi"": ""umb://element/5fc866c590be4d01a28a979472a1ffee"", + ""contentKey"": ""96a15ca9-3970-4e0a-9c66-18433bc23274"", ""areas"": [], ""columnSpan"": 3, ""rowSpan"": 1 @@ -376,25 +377,25 @@ public class BlockEditorComponentTests }, { ""key"": ""2bdcdadd-f609-4acc-b840-01970b9ced1d"", ""items"": [{ - ""contentUdi"": ""umb://element/264536b65b0f4641aa43d4bfb515831d"", + ""contentKey"": ""3093f7f1-c931-4325-ba71-638eb2746c8d"", ""areas"": [], ""columnSpan"": 3, ""rowSpan"": 1, - ""settingsUdi"": ""umb://element/20d735c7c57b40229ed845375cf22d1f"" + ""settingsKey"": ""bef9eb67-56de-4fec-9fbc-1c7c02f5a5a7"" } ] } ], ""columnSpan"": 6, ""rowSpan"": 1, - ""settingsUdi"": ""umb://element/4d121eaba49c4e09a7460069d1bee600"" + ""settingsKey"": ""6eed3662-6ad1-4cba-805b-352f28599b0d"" }, { - ""contentUdi"": ""umb://element/76e24aeb6eeb4370892ca521932a96df"", + ""contentKey"": ""1f778485-933e-40b4-91e2-9926857a5c81"", ""areas"": [], ""columnSpan"": 6, ""rowSpan"": 1 }, { - ""contentUdi"": ""umb://element/90549d94555647fdbe4d111c7178ada4"", + ""contentKey"": ""2d5c6555-0dd8-4db2-b0c9-2d2eba29026d"", ""areas"": [{ ""key"": ""b17663f0-c1f4-4bee-97cd-290fbc7b9a2c"", ""items"": [] @@ -405,51 +406,67 @@ public class BlockEditorComponentTests ], ""columnSpan"": 12, ""rowSpan"": 3, - ""settingsUdi"": ""umb://element/3dfabc96584c4c35ac2e6bf06ffa20de"" + ""settingsKey"": ""48a7b7da-673f-44d5-8bad-7d71d157fb3e"" } ] }, ""contentData"": [{ ""contentTypeKey"": ""36ccf44a-aac8-40a6-8685-73ab03bc9709"", - ""udi"": ""umb://element/d05861169d124582a7c2826e52a51b47"", - ""title"": ""Element one - 12 cols"" + ""key"": ""fb0595b1-26e7-493f-86c7-bf2c42326850"", + ""values"": [ + { ""alias"": ""title"", ""value"": ""Element one - 12 cols"" } + ] }, { ""contentTypeKey"": ""36ccf44a-aac8-40a6-8685-73ab03bc9709"", - ""udi"": ""umb://element/5abad9f1b4e24d7aa269fbd1b50033ac"", - ""title"": ""Element one - 6 cols, left side"" + ""key"": ""4852d9ef-ac8d-4d44-87c9-82d282aa0e7f"", + ""values"": [ + { ""alias"": ""title"", ""value"": ""Element one - 6 cols, left side"" } + ] }, { ""contentTypeKey"": ""5cc488aa-ba24-41f2-a01e-8f2d1982f865"", - ""udi"": ""umb://element/76e24aeb6eeb4370892ca521932a96df"", - ""text"": ""Element two - 6 cols, right side"" + ""key"": ""1f778485-933e-40b4-91e2-9926857a5c81"", + ""values"": [ + { ""alias"": ""title"", ""value"": ""Element one - 6 cols, right side"" } + ] }, { ""contentTypeKey"": ""36ccf44a-aac8-40a6-8685-73ab03bc9709"", - ""udi"": ""umb://element/90549d94555647fdbe4d111c7178ada4"", - ""title"": ""One more element one - 12 cols"", - ""subFeatures"": " + subFeatures.OrIfNullOrWhiteSpace(@"""""") + @" + ""key"": ""2d5c6555-0dd8-4db2-b0c9-2d2eba29026d"", + ""values"": [ + { ""alias"": ""title"", ""value"": ""One more element one - 12 cols"" }, + { ""alias"": ""subFeatures"", ""value"": " + subFeatures.OrIfNullOrWhiteSpace(@"""""") + @" } + ] }, { ""contentTypeKey"": ""5cc488aa-ba24-41f2-a01e-8f2d1982f865"", - ""udi"": ""umb://element/5fc866c590be4d01a28a979472a1ffee"", - ""text"": ""Nested element two - left side"" + ""key"": ""96a15ca9-3970-4e0a-9c66-18433bc23274"", + ""values"": [ + { ""alias"": ""title"", ""value"": ""Nested element two - left side"" } + ] }, { ""contentTypeKey"": ""36ccf44a-aac8-40a6-8685-73ab03bc9709"", - ""udi"": ""umb://element/264536b65b0f4641aa43d4bfb515831d"", - ""title"": ""Nested element one - right side"" + ""key"": ""3093f7f1-c931-4325-ba71-638eb2746c8d"", + ""values"": [ + { ""alias"": ""title"", ""value"": ""Nested element one - right side"" } + ] } ], ""settingsData"": [{ ""contentTypeKey"": ""ef150524-7145-469e-8d99-166aad69a7ad"", - ""udi"": ""umb://element/262d5efd2eeb43ed95e95c094c45ce1c"", - ""enabled"": 1 + ""key"": ""0183ae81-2b62-49b5-8ac6-88d66c33068c"", + ""values"": [ + { ""alias"": ""enabled"", ""value"": 1 } + ] }, { ""contentTypeKey"": ""ef150524-7145-469e-8d99-166aad69a7ad"", - ""udi"": ""umb://element/4d121eaba49c4e09a7460069d1bee600"" + ""key"": ""6eed3662-6ad1-4cba-805b-352f28599b0d"" }, { ""contentTypeKey"": ""ef150524-7145-469e-8d99-166aad69a7ad"", - ""udi"": ""umb://element/20d735c7c57b40229ed845375cf22d1f"" + ""key"": ""bef9eb67-56de-4fec-9fbc-1c7c02f5a5a7"" }, { ""contentTypeKey"": ""ef150524-7145-469e-8d99-166aad69a7ad"", - ""udi"": ""umb://element/3dfabc96584c4c35ac2e6bf06ffa20de"", - ""enabled"": 1 + ""key"": ""48a7b7da-673f-44d5-8bad-7d71d157fb3e"", + ""values"": [ + { ""alias"": ""enabled"", ""value"": 1 } + ] } ] }"; @@ -458,7 +475,7 @@ public class BlockEditorComponentTests { foreach ((Guid oldKey, Guid newKey) in guidMap) { - json = json.Replace(oldKey.ToString("N"), newKey.ToString("N")); + json = json.Replace(oldKey.ToString("D"), newKey.ToString("D")); } return json; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs index 5756a05622..6b090ed7c8 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Serialization; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; @@ -35,15 +36,15 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT var editor = CreateConverter(); var config = ConfigForSingle(SettingKey1); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = @" { ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockGrid + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", - ""settingsUdi"": ""umb://element/2D3529EDB47B4B109F6D4B802DD5DFE2"", + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"", + ""settingsKey"": ""2D3529ED-B47B-4B10-9F6D-4B802DD5DFE2"", ""rowSpan"": 1, ""columnSpan"": 12, ""areas"": [] @@ -53,13 +54,18 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT ""contentData"": [ { ""contentTypeKey"": """ + ContentKey1 + @""", - ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""key"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ], ""settingsData"": [ { ""contentTypeKey"": """ + SettingKey1 + @""", - ""udi"": ""umb://element/2D3529EDB47B4B109F6D4B802DD5DFE2"" + ""key"": ""2D3529ED-B47B-4B10-9F6D-4B802DD5DFE2"" + } + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }"; @@ -70,10 +76,10 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT Assert.IsNotNull(converted); Assert.AreEqual(1, converted.Count); Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), converted[0].Content.Key); - Assert.AreEqual(UdiParser.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), converted[0].ContentUdi); + Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), converted[0].ContentKey); Assert.AreEqual(ContentAlias1, converted[0].Content.ContentType.Alias); - Assert.AreEqual(Guid.Parse("2D3529ED-B47B-4B10-9F6D-4B802DD5DFE2"), converted[0].Settings.Key); - Assert.AreEqual(UdiParser.Parse("umb://element/2D3529EDB47B4B109F6D4B802DD5DFE2"), converted[0].SettingsUdi); + Assert.AreEqual(Guid.Parse("2D3529ED-B47B-4B10-9F6D-4B802DD5DFE2"), converted[0].Settings!.Key); + Assert.AreEqual(Guid.Parse("2D3529ED-B47B-4B10-9F6D-4B802DD5DFE2"), converted[0].SettingsKey); Assert.AreEqual(SettingAlias1, converted[0].Settings.ContentType.Alias); } @@ -83,14 +89,14 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT var editor = CreateConverter(); var config = ConfigForSingle(); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = @" { ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockGrid + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"", ""rowSpan"": 1, ""columnSpan"": 12, ""areas"": [] @@ -100,7 +106,12 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT ""contentData"": [ { ""contentTypeKey"": """ + ContentKey1 + @""", - ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""key"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" + } + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }"; @@ -112,7 +123,7 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT Assert.AreEqual(1, converted.Count); var item0 = converted[0].Content; Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key); - Assert.AreEqual(UdiParser.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), converted[0].ContentUdi); + Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), converted[0].ContentKey); Assert.AreEqual("Test1", item0.ContentType.Alias); Assert.IsNull(converted[0].Settings); } @@ -123,14 +134,14 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT var editor = CreateConverter(); var config = ConfigForSingle(); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = @" { ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockGrid + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"", ""rowSpan"": 1, ""columnSpan"": 12, ""areas"": [] @@ -138,19 +149,24 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT ], """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ], ""Some.Custom.BlockEditor"": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }, ""contentData"": [ { ""contentTypeKey"": """ + ContentKey1 + @""", - ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""key"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" + } + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }"; @@ -162,7 +178,7 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT Assert.AreEqual(1, converted.Count); var item0 = converted[0].Content; Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key); - Assert.AreEqual(UdiParser.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), converted[0].ContentUdi); + Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), converted[0].ContentKey); Assert.AreEqual("Test1", item0.ContentType.Alias); Assert.IsNull(converted[0].Settings); } @@ -171,12 +187,15 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT { var publishedSnapshotAccessor = GetPublishedSnapshotAccessor(); var publishedModelFactory = new NoopPublishedModelFactory(); + var blockVarianceHandler = new BlockEditorVarianceHandler(Mock.Of()); var editor = new BlockGridPropertyValueConverter( Mock.Of(), - new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory), + new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory, Mock.Of(), blockVarianceHandler), new SystemTextJsonSerializer(), new ApiElementBuilder(Mock.Of()), - new BlockGridPropertyValueConstructorCache()); + new BlockGridPropertyValueConstructorCache(), + Mock.Of(), + blockVarianceHandler); return editor; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs index 8c4634a4e2..db720fca46 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -24,13 +24,16 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT { var publishedSnapshotAccessor = GetPublishedSnapshotAccessor(); var publishedModelFactory = new NoopPublishedModelFactory(); + var blockVarianceHandler = new BlockEditorVarianceHandler(Mock.Of()); var editor = new BlockListPropertyValueConverter( Mock.Of(), - new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory), + new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory, Mock.Of(), blockVarianceHandler), Mock.Of(), new ApiElementBuilder(Mock.Of()), new SystemTextJsonSerializer(), - new BlockListPropertyValueConstructorCache()); + new BlockListPropertyValueConstructorCache(), + Mock.Of(), + blockVarianceHandler); return editor; } @@ -123,7 +126,7 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT var editor = CreateConverter(); var config = ConfigForMany(); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); string json = null; var converted = @@ -146,7 +149,7 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT var editor = CreateConverter(); var config = ConfigForMany(); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = "{}"; var converted = @@ -170,11 +173,16 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/e7dba547615b4e9ab4ab2a7674845bc9"" + ""contentKey"": ""e7dba547-615b-4e9a-b4ab-2a7674845bc9"" } ] }, - ""contentData"": [] + ""contentData"": [], + ""expose"": [ + { + ""contentKey"": ""e7dba547-615b-4e9a-b4ab-2a7674845bc9"" + } + ] }"; converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; @@ -188,13 +196,18 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/e7dba547615b4e9ab4ab2a7674845bc9"" + ""contentKey"": ""e7dba547-615b-4e9a-b4ab-2a7674845bc9"" } ] }, ""contentData"": [ { - ""udi"": ""umb://element/e7dba547615b4e9ab4ab2a7674845bc9"" + ""key"": ""e7dba547-615b-4e9a-b4ab-2a7674845bc9"" + } + ], + ""expose"": [ + { + ""contentKey"": ""e7dba547-615b-4e9a-b4ab-2a7674845bc9"" } ] }"; @@ -210,7 +223,7 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }, @@ -219,6 +232,11 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT ""contentTypeKey"": """ + ContentKey1 + @""", ""key"": ""1304E1DD-0000-4396-84FE-8A399231CB3D"" } + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-0000-4396-84FE-8A399231CB3D"" + } ] }"; @@ -234,21 +252,26 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT var editor = CreateConverter(); var config = ConfigForMany(); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = @" { ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }, ""contentData"": [ { ""contentTypeKey"": """ + ContentKey1 + @""", - ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""key"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" + } + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }"; @@ -262,7 +285,7 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key); Assert.AreEqual("Test1", item0.ContentType.Alias); Assert.IsNull(converted[0].Settings); - Assert.AreEqual(UdiParser.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), converted[0].ContentUdi); + Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), converted[0].ContentKey); } [Test] @@ -271,51 +294,61 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT var editor = CreateConverter(); var config = ConfigForMany(); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = @" { ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", - ""settingsUdi"": ""umb://element/1F613E26CE274898908A561437AF5100"" + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"", + ""settingsKey"": ""1F613E26-CE27-4898-908A-561437AF5100"" }, { - ""contentUdi"": ""umb://element/0A4A416E547D464FABCC6F345C17809A"", - ""settingsUdi"": ""umb://element/63027539B0DB45E7B70459762D4E83DD"" + ""contentKey"": ""0A4A416E-547D-464F-ABCC-6F345C17809A"", + ""settingsKey"": ""63027539-B0DB-45E7-B704-59762D4E83DD"" } ] }, ""contentData"": [ { ""contentTypeKey"": """ + ContentKey1 + @""", - ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""key"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" }, { ""contentTypeKey"": """ + ContentKey2 + @""", - ""udi"": ""umb://element/E05A034704424AB3A520E048E6197E79"" + ""key"": ""E05A0347-0442-4AB3-A520-E048E6197E79"" }, { ""contentTypeKey"": """ + ContentKey2 + @""", - ""udi"": ""umb://element/0A4A416E547D464FABCC6F345C17809A"" + ""key"": ""0A4A416E-547D-464F-ABCC-6F345C17809A"" } ], ""settingsData"": [ { ""contentTypeKey"": """ + SettingKey1 + @""", - ""udi"": ""umb://element/63027539B0DB45E7B70459762D4E83DD"" + ""key"": ""63027539-B0DB-45E7-B704-59762D4E83DD"" }, { ""contentTypeKey"": """ + SettingKey2 + @""", - ""udi"": ""umb://element/1F613E26CE274898908A561437AF5100"" + ""key"": ""1F613E26-CE27-4898-908A-561437AF5100"" }, { ""contentTypeKey"": """ + SettingKey2 + @""", - ""udi"": ""umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9"" + ""key"": ""BCF4BA3D-A40C-496C-93EC-58FAC85F18B9"" } - ] -}"; + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" + }, + { + ""contentKey"": ""E05A0347-0442-4AB3-A520-E048E6197E79"" + }, + { + ""contentKey"": ""0A4A416E-547D-464F-ABCC-6F345C17809A"" + } + ]}"; var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as @@ -327,13 +360,13 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT var item0 = converted[0]; Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Content.Key); Assert.AreEqual("Test1", item0.Content.ContentType.Alias); - Assert.AreEqual(Guid.Parse("1F613E26CE274898908A561437AF5100"), item0.Settings.Key); + Assert.AreEqual(Guid.Parse("1F613E26-CE27-4898-908A-561437AF5100"), item0.Settings!.Key); Assert.AreEqual("Setting2", item0.Settings.ContentType.Alias); var item1 = converted[1]; Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Content.Key); Assert.AreEqual("Test2", item1.Content.ContentType.Alias); - Assert.AreEqual(Guid.Parse("63027539B0DB45E7B70459762D4E83DD"), item1.Settings.Key); + Assert.AreEqual(Guid.Parse("63027539-B0DB-45E7-B704-59762D4E83DD"), item1.Settings!.Key); Assert.AreEqual("Setting1", item1.Settings.ContentType.Alias); } @@ -357,48 +390,59 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT }; var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = @" { ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", - ""settingsUdi"": ""umb://element/1F613E26CE274898908A561437AF5100"" + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"", + ""settingsKey"": ""1F613E26-CE27-4898-908A-561437AF5100"" }, { - ""contentUdi"": ""umb://element/0A4A416E547D464FABCC6F345C17809A"", - ""settingsUdi"": ""umb://element/63027539B0DB45E7B70459762D4E83DD"" + ""contentKey"": ""0A4A416E-547D-464F-ABCC-6F345C17809A"", + ""settingsKey"": ""63027539-B0DB-45E7-B704-59762D4E83DD"" } ] }, ""contentData"": [ { ""contentTypeKey"": """ + ContentKey1 + @""", - ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""key"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" }, { ""contentTypeKey"": """ + ContentKey2 + @""", - ""udi"": ""umb://element/E05A034704424AB3A520E048E6197E79"" + ""key"": ""E05A0347-0442-4AB3-A520-E048E6197E79"" }, { ""contentTypeKey"": """ + ContentKey2 + @""", - ""udi"": ""umb://element/0A4A416E547D464FABCC6F345C17809A"" + ""key"": ""0A4A416E-547D-464F-ABCC-6F345C17809A"" } ], ""settingsData"": [ { ""contentTypeKey"": """ + SettingKey1 + @""", - ""udi"": ""umb://element/63027539B0DB45E7B70459762D4E83DD"" + ""key"": ""63027539-B0DB-45E7-B704-59762D4E83DD"" }, { ""contentTypeKey"": """ + SettingKey2 + @""", - ""udi"": ""umb://element/1F613E26CE274898908A561437AF5100"" + ""key"": ""1F613E26-CE27-4898-908A-561437AF5100"" }, { ""contentTypeKey"": """ + SettingKey2 + @""", - ""udi"": ""umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9"" + ""key"": ""BCF4BA3D-A40C-496C-93EC-58FAC85F18B9"" + } + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" + }, + { + ""contentKey"": ""E05A0347-0442-4AB3-A520-E048E6197E79"" + }, + { + ""contentKey"": ""0A4A416E-547D-464F-ABCC-6F345C17809A"" } ] }"; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs index 232b30da26..1e2739c7ac 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs @@ -1,4 +1,5 @@ using Moq; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -61,4 +62,7 @@ public abstract class BlockPropertyValueConverterTestsBase Mock.Of(m => m.ContentType == Mock.Of()); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs index 5c3c761765..48d5c3e0c6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -35,6 +36,7 @@ public class DataValueEditorReuseTests _propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)); _dataValueReferenceFactories = new DataValueReferenceFactoryCollection(Enumerable.Empty); + var blockVarianceHandler = new BlockEditorVarianceHandler(Mock.Of()); _dataValueEditorFactoryMock .Setup(m => m.Create(It.IsAny(), It.IsAny>())) @@ -50,7 +52,8 @@ public class DataValueEditorReuseTests Mock.Of(), Mock.Of(), Mock.Of(), - Mock.Of())); + Mock.Of(), + blockVarianceHandler)); } [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs index 71fa5f085c..72e2d77fa0 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs @@ -27,25 +27,29 @@ public class RichTextPropertyEditorHelperTests { var input = JsonNode.Parse("""" { - "markup": "

this is some markup

", + "markup": "

this is some markup

", "blocks": { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", - "settingsUdi": "umb://element/d2eeef66411142f4a1647a523eaffbc2" + "contentKey": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", + "settingsKey": "d2eeef66-4111-42f4-a164-7a523eaffbc2" } ] }, "contentData": [{ "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1123", - "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", - "contentPropertyAlias": "A content property value" + "key": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", + "values": [ + { "alias": "contentPropertyAlias", "value": "A content property value" } + ] } ], "settingsData": [{ "contentTypeKey": "e7a9447f-e14d-44dd-9ae8-e68c3c3da598", - "udi": "umb://element/d2eeef66411142f4a1647a523eaffbc2", - "settingsPropertyAlias": "A settings property value" + "key": "d2eeef66-4111-42f4-a164-7a523eaffbc2", + "values": [ + { "alias": "settingsPropertyAlias", "value": "A settings property value" } + ] } ] } @@ -55,7 +59,7 @@ public class RichTextPropertyEditorHelperTests var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); Assert.IsTrue(result); Assert.IsNotNull(value); - Assert.AreEqual("

this is some markup

", value.Markup); + Assert.AreEqual("

this is some markup

", value.Markup); Assert.IsNotNull(value.Blocks); @@ -64,16 +68,30 @@ public class RichTextPropertyEditorHelperTests var contentTypeGuid = Guid.Parse("b2f0806c-d231-4c78-88b2-3c97d26e1123"); var itemGuid = Guid.Parse("36cc710a-d8a6-45d0-a07f-7bbd8742cf02"); Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); - Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); Assert.AreEqual(itemGuid, item.Key); + Assert.AreEqual(itemGuid, item.Key); + var contentProperties = value.Blocks.ContentData.First().Values; + Assert.AreEqual(1, contentProperties.Count); + Assert.Multiple(() => + { + Assert.AreEqual("contentPropertyAlias", contentProperties.First().Alias); + Assert.AreEqual("A content property value", contentProperties.First().Value); + }); Assert.AreEqual(1, value.Blocks.SettingsData.Count); item = value.Blocks.SettingsData.Single(); contentTypeGuid = Guid.Parse("e7a9447f-e14d-44dd-9ae8-e68c3c3da598"); itemGuid = Guid.Parse("d2eeef66-4111-42f4-a164-7a523eaffbc2"); Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); - Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); Assert.AreEqual(itemGuid, item.Key); + Assert.AreEqual(itemGuid, item.Key); + var settingsProperties = value.Blocks.SettingsData.First().Values; + Assert.AreEqual(1, settingsProperties.Count); + Assert.Multiple(() => + { + Assert.AreEqual("settingsPropertyAlias", settingsProperties.First().Alias); + Assert.AreEqual("A settings property value", settingsProperties.First().Value); + }); } [Test] @@ -81,25 +99,29 @@ public class RichTextPropertyEditorHelperTests { const string input = """ { - "markup": "

this is some markup

", + "markup": "

this is some markup

", "blocks": { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", - "settingsUdi": "umb://element/d2eeef66411142f4a1647a523eaffbc2" + "contentKey": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", + "settingsKey": "d2eeef66-4111-42f4-a164-7a523eaffbc2" } ] }, "contentData": [{ "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1123", - "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", - "contentPropertyAlias": "A content property value" + "key": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", + "values": [ + { "alias": "contentPropertyAlias", "value": "A content property value" } + ] } ], "settingsData": [{ "contentTypeKey": "e7a9447f-e14d-44dd-9ae8-e68c3c3da598", - "udi": "umb://element/d2eeef66411142f4a1647a523eaffbc2", - "settingsPropertyAlias": "A settings property value" + "key": "d2eeef66-4111-42f4-a164-7a523eaffbc2", + "values": [ + { "alias": "settingsPropertyAlias", "value": "A settings property value" } + ] } ] } @@ -109,7 +131,7 @@ public class RichTextPropertyEditorHelperTests var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); Assert.IsTrue(result); Assert.IsNotNull(value); - Assert.AreEqual("

this is some markup

", value.Markup); + Assert.AreEqual("

this is some markup

", value.Markup); Assert.IsNotNull(value.Blocks); @@ -118,16 +140,30 @@ public class RichTextPropertyEditorHelperTests var contentTypeGuid = Guid.Parse("b2f0806c-d231-4c78-88b2-3c97d26e1123"); var itemGuid = Guid.Parse("36cc710a-d8a6-45d0-a07f-7bbd8742cf02"); Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); - Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); Assert.AreEqual(itemGuid, item.Key); + Assert.AreEqual(itemGuid, item.Key); + var contentProperties = value.Blocks.ContentData.First().Values; + Assert.AreEqual(1, contentProperties.Count); + Assert.Multiple(() => + { + Assert.AreEqual("contentPropertyAlias", contentProperties.First().Alias); + Assert.AreEqual("A content property value", contentProperties.First().Value); + }); Assert.AreEqual(1, value.Blocks.SettingsData.Count); item = value.Blocks.SettingsData.Single(); contentTypeGuid = Guid.Parse("e7a9447f-e14d-44dd-9ae8-e68c3c3da598"); itemGuid = Guid.Parse("d2eeef66-4111-42f4-a164-7a523eaffbc2"); Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); - Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); Assert.AreEqual(itemGuid, item.Key); + Assert.AreEqual(itemGuid, item.Key); + var settingsProperties = value.Blocks.SettingsData.First().Values; + Assert.AreEqual(1, settingsProperties.Count); + Assert.Multiple(() => + { + Assert.AreEqual("settingsPropertyAlias", settingsProperties.First().Alias); + Assert.AreEqual("A settings property value", settingsProperties.First().Value); + }); } [Test] @@ -135,18 +171,20 @@ public class RichTextPropertyEditorHelperTests { const string input = """ { - "markup": "

this is some markup

", + "markup": "

this is some markup

", "blocks": { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02" + "contentKey": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02" } ] }, "contentData": [{ "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1123", - "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", - "contentPropertyAlias": "A content property value" + "key": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", + "values": [ + { "alias": "contentPropertyAlias", "value": "A content property value" } + ] } ], "settingsData": [] @@ -157,7 +195,7 @@ public class RichTextPropertyEditorHelperTests var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); Assert.IsTrue(result); Assert.IsNotNull(value); - Assert.AreEqual("

this is some markup

", value.Markup); + Assert.AreEqual("

this is some markup

", value.Markup); Assert.IsNotNull(value.Blocks); @@ -166,8 +204,15 @@ public class RichTextPropertyEditorHelperTests var contentTypeGuid = Guid.Parse("b2f0806c-d231-4c78-88b2-3c97d26e1123"); var itemGuid = Guid.Parse("36cc710a-d8a6-45d0-a07f-7bbd8742cf02"); Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); - Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); Assert.AreEqual(itemGuid, item.Key); + Assert.AreEqual(itemGuid, item.Key); + var contentProperties = value.Blocks.ContentData.First().Values; + Assert.AreEqual(1, contentProperties.Count); + Assert.Multiple(() => + { + Assert.AreEqual("contentPropertyAlias", contentProperties.First().Alias); + Assert.AreEqual("A content property value", contentProperties.First().Value); + }); Assert.AreEqual(0, value.Blocks.SettingsData.Count); } @@ -177,23 +222,23 @@ public class RichTextPropertyEditorHelperTests { const string input = """ { - "markup": "

this is some markup

", + "markup": "

this is some markup

", "blocks": { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02" + "contentKey": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02" }, { - "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf03" + "contentKey": "36cc710a-d8a6-45d0-a07f-7bbd8742cf03" } ] }, "contentData": [{ "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1123", - "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", + "key": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", "contentPropertyAlias": "A content property value" }, { "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1124", - "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf03", + "key": "36cc710a-d8a6-45d0-a07f-7bbd8742cf03", "contentPropertyAlias": "A content property value" } ], @@ -205,7 +250,7 @@ public class RichTextPropertyEditorHelperTests var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); Assert.IsTrue(result); Assert.IsNotNull(value); - Assert.AreEqual("

this is some markup

", value.Markup); + Assert.AreEqual("

this is some markup

", value.Markup); Assert.IsNotNull(value.Blocks); @@ -216,7 +261,6 @@ public class RichTextPropertyEditorHelperTests for (var i = 0; i < value.Blocks.ContentData.Count; i++) { var item = value.Blocks.ContentData[i]; Assert.AreEqual(contentTypeGuids[i], item.ContentTypeKey); - Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuids[i]), item.Udi); Assert.AreEqual(itemGuids[i], item.Key); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PropertyEditors/BlockEditorVarianceHandlerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PropertyEditors/BlockEditorVarianceHandlerTests.cs new file mode 100644 index 0000000000..664478f16b --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PropertyEditors/BlockEditorVarianceHandlerTests.cs @@ -0,0 +1,75 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PropertyEditors; + +// TODO KJA: more tests for BlockEditorVarianceHandler +[TestFixture] +public class BlockEditorVarianceHandlerTests +{ + [Test] + public async Task Assigns_Default_Culture_When_Culture_Variance_Is_Enabled() + { + var propertyValue = new BlockPropertyValue { Culture = null }; + var subject = BlockEditorVarianceHandler("da-DK"); + var result = await subject.AlignedPropertyVarianceAsync( + propertyValue, + PublishedPropertyType(ContentVariation.Culture), + PublishedElement(ContentVariation.Culture)); + Assert.IsNotNull(result); + Assert.AreEqual("da-DK", result.Culture); + } + + [Test] + public async Task Removes_Default_Culture_When_Culture_Variance_Is_Disabled() + { + var propertyValue = new BlockPropertyValue { Culture = "da-DK" }; + var subject = BlockEditorVarianceHandler("da-DK"); + var result = await subject.AlignedPropertyVarianceAsync( + propertyValue, + PublishedPropertyType(ContentVariation.Nothing), + PublishedElement(ContentVariation.Nothing)); + Assert.IsNotNull(result); + Assert.AreEqual(null, result.Culture); + } + + [Test] + public async Task Ignores_NonDefault_Culture_When_Culture_Variance_Is_Disabled() + { + var propertyValue = new BlockPropertyValue { Culture = "en-US" }; + var subject = BlockEditorVarianceHandler("da-DK"); + var result = await subject.AlignedPropertyVarianceAsync( + propertyValue, + PublishedPropertyType(ContentVariation.Nothing), + PublishedElement(ContentVariation.Nothing)); + Assert.IsNull(result); + } + + private static IPublishedPropertyType PublishedPropertyType(ContentVariation variation) + { + var propertyTypeMock = new Mock(); + propertyTypeMock.SetupGet(m => m.Variations).Returns(variation); + return propertyTypeMock.Object; + } + + private static IPublishedElement PublishedElement(ContentVariation variation) + { + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(m => m.Variations).Returns(variation); + var elementMock = new Mock(); + elementMock.SetupGet(m => m.ContentType).Returns(contentTypeMock.Object); + return elementMock.Object; + } + + private static BlockEditorVarianceHandler BlockEditorVarianceHandler(string defaultLanguageIsoCode) + { + var languageServiceMock = new Mock(); + languageServiceMock.Setup(m => m.GetDefaultIsoCodeAsync()).ReturnsAsync(defaultLanguageIsoCode); + return new BlockEditorVarianceHandler(languageServiceMock.Object); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs index 09000d474e..7377efdf35 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs @@ -11,14 +11,14 @@ public class JsonBlockValueConverterTests [Test] public void Can_Serialize_BlockGrid_With_Blocks() { - var contentElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var settingsElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var contentElementUdi2 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var settingsElementUdi2 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var contentElementUdi3 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var settingsElementUdi3 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var contentElementUdi4 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var settingsElementUdi4 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementKey1 = Guid.NewGuid(); + var settingsElementKey1 = Guid.NewGuid(); + var contentElementKey2 = Guid.NewGuid(); + var settingsElementKey2 = Guid.NewGuid(); + var contentElementKey3 = Guid.NewGuid(); + var settingsElementKey3 = Guid.NewGuid(); + var contentElementKey4 = Guid.NewGuid(); + var settingsElementKey4 = Guid.NewGuid(); var elementType1Key = Guid.NewGuid(); var elementType2Key = Guid.NewGuid(); @@ -27,7 +27,7 @@ public class JsonBlockValueConverterTests var blockGridValue = new BlockGridValue( [ - new BlockGridLayoutItem(contentElementUdi1, settingsElementUdi1) + new BlockGridLayoutItem(contentElementKey1, settingsElementKey1) { ColumnSpan = 123, RowSpan = 456, @@ -37,7 +37,7 @@ public class JsonBlockValueConverterTests { Items = [ - new BlockGridLayoutItem(contentElementUdi3, settingsElementUdi3) + new BlockGridLayoutItem(contentElementKey3, settingsElementKey3) { ColumnSpan = 12, RowSpan = 34, @@ -47,7 +47,7 @@ public class JsonBlockValueConverterTests { Items = [ - new BlockGridLayoutItem(contentElementUdi4, settingsElementUdi4) + new BlockGridLayoutItem(contentElementKey4, settingsElementKey4) { ColumnSpan = 56, RowSpan = 78, @@ -60,7 +60,7 @@ public class JsonBlockValueConverterTests }, ], }, - new BlockGridLayoutItem(contentElementUdi2, settingsElementUdi2) + new BlockGridLayoutItem(contentElementKey2, settingsElementKey2) { ColumnSpan = 789, RowSpan = 123, @@ -69,17 +69,17 @@ public class JsonBlockValueConverterTests { ContentData = [ - new(contentElementUdi1, elementType1Key, "elementType1"), - new(contentElementUdi2, elementType2Key, "elementType2"), - new(contentElementUdi3, elementType3Key, "elementType3"), - new(contentElementUdi4, elementType4Key, "elementType4"), + new(contentElementKey1, elementType1Key, "elementType1"), + new(contentElementKey2, elementType2Key, "elementType2"), + new(contentElementKey3, elementType3Key, "elementType3"), + new(contentElementKey4, elementType4Key, "elementType4"), ], SettingsData = [ - new(settingsElementUdi1, elementType3Key, "elementType3"), - new(settingsElementUdi2, elementType4Key, "elementType4"), - new(settingsElementUdi3, elementType1Key, "elementType1"), - new(settingsElementUdi4, elementType2Key, "elementType2") + new(settingsElementKey1, elementType3Key, "elementType3"), + new(settingsElementKey2, elementType4Key, "elementType4"), + new(settingsElementKey3, elementType1Key, "elementType1"), + new(settingsElementKey4, elementType2Key, "elementType2") ] }; @@ -97,13 +97,13 @@ public class JsonBlockValueConverterTests { Assert.AreEqual(123, layoutItems[0].ColumnSpan); Assert.AreEqual(456, layoutItems[0].RowSpan); - Assert.AreEqual(contentElementUdi1, layoutItems[0].ContentUdi); - Assert.AreEqual(settingsElementUdi1, layoutItems[0].SettingsUdi); + Assert.AreEqual(contentElementKey1, layoutItems[0].ContentKey); + Assert.AreEqual(settingsElementKey1, layoutItems[0].SettingsKey); Assert.AreEqual(789, layoutItems[1].ColumnSpan); Assert.AreEqual(123, layoutItems[1].RowSpan); - Assert.AreEqual(contentElementUdi2, layoutItems[1].ContentUdi); - Assert.AreEqual(settingsElementUdi2, layoutItems[1].SettingsUdi); + Assert.AreEqual(contentElementKey2, layoutItems[1].ContentKey); + Assert.AreEqual(settingsElementKey2, layoutItems[1].SettingsKey); }); Assert.AreEqual(1, layoutItems[0].Areas.Length); @@ -113,8 +113,8 @@ public class JsonBlockValueConverterTests { Assert.AreEqual(12, layoutItems[0].Areas[0].Items[0].ColumnSpan); Assert.AreEqual(34, layoutItems[0].Areas[0].Items[0].RowSpan); - Assert.AreEqual(contentElementUdi3, layoutItems[0].Areas[0].Items[0].ContentUdi); - Assert.AreEqual(settingsElementUdi3, layoutItems[0].Areas[0].Items[0].SettingsUdi); + Assert.AreEqual(contentElementKey3, layoutItems[0].Areas[0].Items[0].ContentKey); + Assert.AreEqual(settingsElementKey3, layoutItems[0].Areas[0].Items[0].SettingsKey); }); Assert.AreEqual(1, layoutItems[0].Areas[0].Items[0].Areas.Length); @@ -124,26 +124,26 @@ public class JsonBlockValueConverterTests { Assert.AreEqual(56, layoutItems[0].Areas[0].Items[0].Areas[0].Items[0].ColumnSpan); Assert.AreEqual(78, layoutItems[0].Areas[0].Items[0].Areas[0].Items[0].RowSpan); - Assert.AreEqual(contentElementUdi4, layoutItems[0].Areas[0].Items[0].Areas[0].Items[0].ContentUdi); - Assert.AreEqual(settingsElementUdi4, layoutItems[0].Areas[0].Items[0].Areas[0].Items[0].SettingsUdi); + Assert.AreEqual(contentElementKey4, layoutItems[0].Areas[0].Items[0].Areas[0].Items[0].ContentKey); + Assert.AreEqual(settingsElementKey4, layoutItems[0].Areas[0].Items[0].Areas[0].Items[0].SettingsKey); }); Assert.AreEqual(4, deserialized.ContentData.Count); Assert.Multiple(() => { - Assert.AreEqual(contentElementUdi1, deserialized.ContentData[0].Udi); + Assert.AreEqual(contentElementKey1, deserialized.ContentData[0].Key); Assert.AreEqual(elementType1Key, deserialized.ContentData[0].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.ContentData[0].ContentTypeAlias); // explicitly annotated to be ignored by the serializer - Assert.AreEqual(contentElementUdi2, deserialized.ContentData[1].Udi); + Assert.AreEqual(contentElementKey2, deserialized.ContentData[1].Key); Assert.AreEqual(elementType2Key, deserialized.ContentData[1].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.ContentData[1].ContentTypeAlias); - Assert.AreEqual(contentElementUdi3, deserialized.ContentData[2].Udi); + Assert.AreEqual(contentElementKey3, deserialized.ContentData[2].Key); Assert.AreEqual(elementType3Key, deserialized.ContentData[2].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.ContentData[2].ContentTypeAlias); - Assert.AreEqual(contentElementUdi3, deserialized.ContentData[2].Udi); + Assert.AreEqual(contentElementKey3, deserialized.ContentData[2].Key); Assert.AreEqual(elementType3Key, deserialized.ContentData[2].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.ContentData[2].ContentTypeAlias); }); @@ -151,19 +151,19 @@ public class JsonBlockValueConverterTests Assert.AreEqual(4, deserialized.SettingsData.Count); Assert.Multiple(() => { - Assert.AreEqual(settingsElementUdi1, deserialized.SettingsData[0].Udi); + Assert.AreEqual(settingsElementKey1, deserialized.SettingsData[0].Key); Assert.AreEqual(elementType3Key, deserialized.SettingsData[0].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.SettingsData[0].ContentTypeAlias); - Assert.AreEqual(settingsElementUdi2, deserialized.SettingsData[1].Udi); + Assert.AreEqual(settingsElementKey2, deserialized.SettingsData[1].Key); Assert.AreEqual(elementType4Key, deserialized.SettingsData[1].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.SettingsData[1].ContentTypeAlias); - Assert.AreEqual(settingsElementUdi3, deserialized.SettingsData[2].Udi); + Assert.AreEqual(settingsElementKey3, deserialized.SettingsData[2].Key); Assert.AreEqual(elementType1Key, deserialized.SettingsData[2].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.SettingsData[2].ContentTypeAlias); - Assert.AreEqual(settingsElementUdi4, deserialized.SettingsData[3].Udi); + Assert.AreEqual(settingsElementKey4, deserialized.SettingsData[3].Key); Assert.AreEqual(elementType2Key, deserialized.SettingsData[3].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.SettingsData[3].ContentTypeAlias); }); @@ -189,10 +189,10 @@ public class JsonBlockValueConverterTests [Test] public void Can_Serialize_BlockList_With_Blocks() { - var contentElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var settingsElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var contentElementUdi2 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var settingsElementUdi2 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementKey1 = Guid.NewGuid(); + var settingsElementKey1 = Guid.NewGuid(); + var contentElementKey2 = Guid.NewGuid(); + var settingsElementKey2 = Guid.NewGuid(); var elementType1Key = Guid.NewGuid(); var elementType2Key = Guid.NewGuid(); @@ -201,19 +201,19 @@ public class JsonBlockValueConverterTests var blockListValue = new BlockListValue( [ - new BlockListLayoutItem(contentElementUdi1, settingsElementUdi1), - new BlockListLayoutItem(contentElementUdi2, settingsElementUdi2), + new BlockListLayoutItem(contentElementKey1, settingsElementKey1), + new BlockListLayoutItem(contentElementKey2, settingsElementKey2), ]) { ContentData = [ - new(contentElementUdi1, elementType1Key, "elementType1"), - new(contentElementUdi2, elementType2Key, "elementType2") + new(contentElementKey1, elementType1Key, "elementType1"), + new(contentElementKey2, elementType2Key, "elementType2") ], SettingsData = [ - new(settingsElementUdi1, elementType3Key, "elementType3"), - new(settingsElementUdi2, elementType4Key, "elementType4") + new(settingsElementKey1, elementType3Key, "elementType3"), + new(settingsElementKey2, elementType4Key, "elementType4") ] }; @@ -229,21 +229,21 @@ public class JsonBlockValueConverterTests Assert.AreEqual(2, layoutItems.Count()); Assert.Multiple(() => { - Assert.AreEqual(contentElementUdi1, layoutItems.First().ContentUdi); - Assert.AreEqual(settingsElementUdi1, layoutItems.First().SettingsUdi); + Assert.AreEqual(contentElementKey1, layoutItems.First().ContentKey); + Assert.AreEqual(settingsElementKey1, layoutItems.First().SettingsKey); - Assert.AreEqual(contentElementUdi2, layoutItems.Last().ContentUdi); - Assert.AreEqual(settingsElementUdi2, layoutItems.Last().SettingsUdi); + Assert.AreEqual(contentElementKey2, layoutItems.Last().ContentKey); + Assert.AreEqual(settingsElementKey2, layoutItems.Last().SettingsKey); }); Assert.AreEqual(2, deserialized.ContentData.Count); Assert.Multiple(() => { - Assert.AreEqual(contentElementUdi1, deserialized.ContentData.First().Udi); + Assert.AreEqual(contentElementKey1, deserialized.ContentData.First().Key); Assert.AreEqual(elementType1Key, deserialized.ContentData.First().ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.ContentData.First().ContentTypeAlias); // explicitly annotated to be ignored by the serializer - Assert.AreEqual(contentElementUdi2, deserialized.ContentData.Last().Udi); + Assert.AreEqual(contentElementKey2, deserialized.ContentData.Last().Key); Assert.AreEqual(elementType2Key, deserialized.ContentData.Last().ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.ContentData.Last().ContentTypeAlias); }); @@ -251,11 +251,11 @@ public class JsonBlockValueConverterTests Assert.AreEqual(2, deserialized.SettingsData.Count); Assert.Multiple(() => { - Assert.AreEqual(settingsElementUdi1, deserialized.SettingsData.First().Udi); + Assert.AreEqual(settingsElementKey1, deserialized.SettingsData.First().Key); Assert.AreEqual(elementType3Key, deserialized.SettingsData.First().ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.SettingsData.First().ContentTypeAlias); - Assert.AreEqual(settingsElementUdi2, deserialized.SettingsData.Last().Udi); + Assert.AreEqual(settingsElementKey2, deserialized.SettingsData.Last().Key); Assert.AreEqual(elementType4Key, deserialized.SettingsData.Last().ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.SettingsData.Last().ContentTypeAlias); }); @@ -281,10 +281,10 @@ public class JsonBlockValueConverterTests [Test] public void Can_Serialize_Richtext_With_Blocks() { - var contentElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var settingsElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var contentElementUdi2 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var settingsElementUdi2 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementKey1 = Guid.NewGuid(); + var settingsElementKey1 = Guid.NewGuid(); + var contentElementKey2 = Guid.NewGuid(); + var settingsElementKey2 = Guid.NewGuid(); var elementType1Key = Guid.NewGuid(); var elementType2Key = Guid.NewGuid(); @@ -293,19 +293,19 @@ public class JsonBlockValueConverterTests var richTextBlockValue = new RichTextBlockValue( [ - new RichTextBlockLayoutItem(contentElementUdi1, settingsElementUdi1), - new RichTextBlockLayoutItem(contentElementUdi2, settingsElementUdi2), + new RichTextBlockLayoutItem(contentElementKey1, settingsElementKey1), + new RichTextBlockLayoutItem(contentElementKey2, settingsElementKey2), ]) { ContentData = [ - new(contentElementUdi1, elementType1Key, "elementType1"), - new(contentElementUdi2, elementType2Key, "elementType2") + new(contentElementKey1, elementType1Key, "elementType1"), + new(contentElementKey2, elementType2Key, "elementType2") ], SettingsData = [ - new(settingsElementUdi1, elementType3Key, "elementType3"), - new(settingsElementUdi2, elementType4Key, "elementType4") + new(settingsElementKey1, elementType3Key, "elementType3"), + new(settingsElementKey2, elementType4Key, "elementType4") ] }; @@ -325,26 +325,26 @@ public class JsonBlockValueConverterTests var deserializedBlocks = deserialized.Blocks; Assert.IsNotNull(deserializedBlocks); Assert.AreEqual(1, deserializedBlocks.Layout.Count); - Assert.IsTrue(deserializedBlocks.Layout.ContainsKey(Constants.PropertyEditors.Aliases.TinyMce)); - var layoutItems = deserializedBlocks.Layout[Constants.PropertyEditors.Aliases.TinyMce].OfType().ToArray(); + Assert.IsTrue(deserializedBlocks.Layout.ContainsKey(Constants.PropertyEditors.Aliases.RichText)); + var layoutItems = deserializedBlocks.Layout[Constants.PropertyEditors.Aliases.RichText].OfType().ToArray(); Assert.AreEqual(2, layoutItems.Count()); Assert.Multiple(() => { - Assert.AreEqual(contentElementUdi1, layoutItems.First().ContentUdi); - Assert.AreEqual(settingsElementUdi1, layoutItems.First().SettingsUdi); + Assert.AreEqual(contentElementKey1, layoutItems.First().ContentKey); + Assert.AreEqual(settingsElementKey1, layoutItems.First().SettingsKey); - Assert.AreEqual(contentElementUdi2, layoutItems.Last().ContentUdi); - Assert.AreEqual(settingsElementUdi2, layoutItems.Last().SettingsUdi); + Assert.AreEqual(contentElementKey2, layoutItems.Last().ContentKey); + Assert.AreEqual(settingsElementKey2, layoutItems.Last().SettingsKey); }); Assert.AreEqual(2, deserializedBlocks.ContentData.Count); Assert.Multiple(() => { - Assert.AreEqual(contentElementUdi1, deserializedBlocks.ContentData.First().Udi); + Assert.AreEqual(contentElementKey1, deserializedBlocks.ContentData.First().Key); Assert.AreEqual(elementType1Key, deserializedBlocks.ContentData.First().ContentTypeKey); Assert.AreEqual(string.Empty, deserializedBlocks.ContentData.First().ContentTypeAlias); // explicitly annotated to be ignored by the serializer - Assert.AreEqual(contentElementUdi2, deserializedBlocks.ContentData.Last().Udi); + Assert.AreEqual(contentElementKey2, deserializedBlocks.ContentData.Last().Key); Assert.AreEqual(elementType2Key, deserializedBlocks.ContentData.Last().ContentTypeKey); Assert.AreEqual(string.Empty, deserializedBlocks.ContentData.Last().ContentTypeAlias); }); @@ -352,11 +352,11 @@ public class JsonBlockValueConverterTests Assert.AreEqual(2, deserializedBlocks.SettingsData.Count); Assert.Multiple(() => { - Assert.AreEqual(settingsElementUdi1, deserializedBlocks.SettingsData.First().Udi); + Assert.AreEqual(settingsElementKey1, deserializedBlocks.SettingsData.First().Key); Assert.AreEqual(elementType3Key, deserializedBlocks.SettingsData.First().ContentTypeKey); Assert.AreEqual(string.Empty, deserializedBlocks.SettingsData.First().ContentTypeAlias); - Assert.AreEqual(settingsElementUdi2, deserializedBlocks.SettingsData.Last().Udi); + Assert.AreEqual(settingsElementKey2, deserializedBlocks.SettingsData.Last().Key); Assert.AreEqual(elementType4Key, deserializedBlocks.SettingsData.Last().ContentTypeKey); Assert.AreEqual(string.Empty, deserializedBlocks.SettingsData.Last().ContentTypeAlias); }); @@ -389,39 +389,39 @@ public class JsonBlockValueConverterTests [Test] public void Ignores_Other_Layouts() { - var contentElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); - var settingsElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementKey1 = Guid.NewGuid(); + var settingsElementKey1 = Guid.NewGuid(); var elementType1Key = Guid.NewGuid(); var elementType2Key = Guid.NewGuid(); var blockListValue = new BlockListValue( [ - new BlockListLayoutItem(contentElementUdi1, settingsElementUdi1), + new BlockListLayoutItem(contentElementKey1, settingsElementKey1), ]) { Layout = { - [Constants.PropertyEditors.Aliases.TinyMce] = + [Constants.PropertyEditors.Aliases.RichText] = [ - new RichTextBlockLayoutItem(contentElementUdi1, settingsElementUdi1) + new RichTextBlockLayoutItem(contentElementKey1, settingsElementKey1) ], [Constants.PropertyEditors.Aliases.BlockGrid] = [ - new BlockGridLayoutItem(contentElementUdi1, settingsElementUdi1), + new BlockGridLayoutItem(contentElementKey1, settingsElementKey1), ], ["Some.Custom.Block.Editor"] = [ - new BlockListLayoutItem(contentElementUdi1, settingsElementUdi1), + new BlockListLayoutItem(contentElementKey1, settingsElementKey1), ] }, ContentData = [ - new(contentElementUdi1, elementType1Key, "elementType1"), + new(contentElementKey1, elementType1Key, "elementType1"), ], SettingsData = [ - new(settingsElementUdi1, elementType2Key, "elementType2") + new(settingsElementKey1, elementType2Key, "elementType2") ] }; @@ -437,8 +437,8 @@ public class JsonBlockValueConverterTests Assert.AreEqual(1, layoutItems.Count()); Assert.Multiple(() => { - Assert.AreEqual(contentElementUdi1, layoutItems.First().ContentUdi); - Assert.AreEqual(settingsElementUdi1, layoutItems.First().SettingsUdi); + Assert.AreEqual(contentElementKey1, layoutItems.First().ContentKey); + Assert.AreEqual(settingsElementKey1, layoutItems.First().SettingsKey); }); } } From b4961868b49ac33231fd9b85610e037f5f4b0bb6 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:04:30 +0200 Subject: [PATCH 07/25] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 3b8ec46d3a..510515b46c 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 3b8ec46d3aa9ccb09e476ac8767c500fec6d4e89 +Subproject commit 510515b46c4f5f869554601701fa0a7ec4c88027 From 79ff0e04b87747d1f0d601c327b59468e815a286 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 30 Sep 2024 11:42:33 +0200 Subject: [PATCH 08/25] Swagger sub types selectors (take 2) (#17132) * Initial implementation * Remove conflicting constructor (was obsolete for 15 anyway) * Don't use primary constructors * Fix swagger path segment qualifier * Make non-interface method protected * Use constant for splitting string * Update document name parsing --------- Co-authored-by: mattbrailsford --- .../ConfigureUmbracoSwaggerGenOptions.cs | 21 ++++--- .../UmbracoBuilderApiExtensions.cs | 2 + .../OpenApi/ISubTypesHandler.cs | 8 +++ .../OpenApi/ISubTypesSelector.cs | 6 ++ .../OpenApi/SubTypesHandler.cs | 20 +++++++ .../OpenApi/SubTypesSelector.cs | 60 +++++++++++++++++++ ...reUmbracoManagementApiSwaggerGenOptions.cs | 1 - 7 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesHandler.cs create mode 100644 src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesSelector.cs create mode 100644 src/Umbraco.Cms.Api.Common/OpenApi/SubTypesHandler.cs create mode 100644 src/Umbraco.Cms.Api.Common/OpenApi/SubTypesSelector.cs diff --git a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs index 572a747179..0128ef6ada 100644 --- a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Api.Common.OpenApi; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Api.Common.Configuration; @@ -14,22 +15,23 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions apiVersioningOptions, - IOperationIdSelector operationIdSelector, - ISchemaIdSelector schemaIdSelector) - : this(operationIdSelector, schemaIdSelector) - { - } - + [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16.")] public ConfigureUmbracoSwaggerGenOptions( IOperationIdSelector operationIdSelector, ISchemaIdSelector schemaIdSelector) + : this(operationIdSelector, schemaIdSelector, StaticServiceProvider.Instance.GetRequiredService()) + { } + + public ConfigureUmbracoSwaggerGenOptions( + IOperationIdSelector operationIdSelector, + ISchemaIdSelector schemaIdSelector, + ISubTypesSelector subTypesSelector) { _operationIdSelector = operationIdSelector; _schemaIdSelector = schemaIdSelector; + _subTypesSelector = subTypesSelector; } public void Configure(SwaggerGenOptions swaggerGenOptions) @@ -62,6 +64,7 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions(); swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId); + swaggerGenOptions.SelectSubTypesUsing(_subTypesSelector.SubTypes); swaggerGenOptions.SupportNonNullableReferenceTypes(); } diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs index 49fe1f233e..fcd7d9c6ec 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs @@ -23,6 +23,8 @@ public static class UmbracoBuilderApiExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.Configure(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon"))); return builder; diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesHandler.cs b/src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesHandler.cs new file mode 100644 index 0000000000..4a6ca4109a --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesHandler.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Common.OpenApi; + +public interface ISubTypesHandler +{ + bool CanHandle(Type type, string documentName); + + IEnumerable Handle(Type type); +} diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesSelector.cs b/src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesSelector.cs new file mode 100644 index 0000000000..164b6196aa --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesSelector.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Common.OpenApi; + +public interface ISubTypesSelector +{ + IEnumerable SubTypes(Type type); +} diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/SubTypesHandler.cs b/src/Umbraco.Cms.Api.Common/OpenApi/SubTypesHandler.cs new file mode 100644 index 0000000000..ffba71aaf8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/SubTypesHandler.cs @@ -0,0 +1,20 @@ +using Umbraco.Cms.Api.Common.Serialization; + +namespace Umbraco.Cms.Api.Common.OpenApi; + +public class SubTypesHandler : ISubTypesHandler +{ + private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver; + + public SubTypesHandler(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver) + => _umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver; + + protected virtual bool CanHandle(Type type) + => type.Namespace?.StartsWith("Umbraco.Cms") is true; + + public virtual bool CanHandle(Type type, string documentName) + => CanHandle(type); + + public virtual IEnumerable Handle(Type type) + => _umbracoJsonTypeInfoResolver.FindSubTypes(type); +} diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/SubTypesSelector.cs b/src/Umbraco.Cms.Api.Common/OpenApi/SubTypesSelector.cs new file mode 100644 index 0000000000..c6e172f3f7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/SubTypesSelector.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Api.Common.Serialization; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Common.OpenApi; + +public class SubTypesSelector : ISubTypesSelector +{ + private readonly IOptions _settings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IEnumerable _subTypeHandlers; + private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver; + + public SubTypesSelector( + IOptions settings, + IHostingEnvironment hostingEnvironment, + IHttpContextAccessor httpContextAccessor, + IEnumerable subTypeHandlers, + IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver) + { + _settings = settings; + _hostingEnvironment = hostingEnvironment; + _httpContextAccessor = httpContextAccessor; + _subTypeHandlers = subTypeHandlers; + _umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver; + } + + public IEnumerable SubTypes(Type type) + { + var backOfficePath = _settings.Value.GetBackOfficePath(_hostingEnvironment); + var swaggerPath = $"{backOfficePath}/swagger"; + + if (_httpContextAccessor.HttpContext?.Request.Path.StartsWithSegments(swaggerPath) ?? false) + { + // Split the path into segments + var segments = _httpContextAccessor.HttpContext.Request.Path.Value! + .Substring(swaggerPath.Length) + .TrimStart(Constants.CharArrays.ForwardSlash) + .Split(Constants.CharArrays.ForwardSlash); + + // Extract the document name from the path + var documentName = segments[0]; + + // Find the first handler that can handle the type / document name combination + ISubTypesHandler? handler = _subTypeHandlers.FirstOrDefault(h => h.CanHandle(type, documentName)); + if (handler != null) + { + return handler.Handle(type); + } + } + + // Default implementation to maintain backwards compatibility + return _umbracoJsonTypeInfoResolver.FindSubTypes(type); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs index 6b67c805af..74862e3bab 100644 --- a/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs @@ -31,7 +31,6 @@ public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions< }); swaggerGenOptions.OperationFilter(); - swaggerGenOptions.SelectSubTypesUsing(_umbracoJsonTypeInfoResolver.FindSubTypes); swaggerGenOptions.UseOneOfForPolymorphism(); // Ensure all types that implements the IOpenApiDiscriminator have a $type property in the OpenApi schema with the default value (The class name) that is expected by the server From 57f2bee42432669e84df519b6b5d4aa03a7f719a Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:22:59 +0200 Subject: [PATCH 09/25] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 510515b46c..fbc99dd047 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 510515b46c4f5f869554601701fa0a7ec4c88027 +Subproject commit fbc99dd04768c96b55161c71f8bf0f6b3594815b From 517050d9017681f64fe714145efe1520f62ce031 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:30:59 +0200 Subject: [PATCH 10/25] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index fbc99dd047..5a13b7741d 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit fbc99dd04768c96b55161c71f8bf0f6b3594815b +Subproject commit 5a13b7741d681cf3d24e8ff5616b1000ade19f93 From 04ba12297fa3bc147c43ba765d86fde88e700176 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 30 Sep 2024 16:43:05 +0200 Subject: [PATCH 11/25] Handle navigation updates in cache refeshers (#17161) * Handle navigation updates in cache refeshers * Same for media cache refreshers * Clean up * More clean up and renaming content to media * Update src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> --------- Co-authored-by: Elitsa Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> --- .../Implement/ContentCacheRefresher.cs | 109 +++++++++++++- .../Implement/MediaCacheRefresher.cs | 138 ++++++++++++++++-- .../Factories/NavigationFactory.cs | 42 ------ src/Umbraco.Core/Services/ContentService.cs | 116 --------------- src/Umbraco.Core/Services/MediaService.cs | 117 +-------------- .../ContentNavigationServiceBase.cs | 72 ++++++++- .../DocumentNavigationServiceTestsBase.cs | 11 ++ .../MediaNavigationServiceTestsBase.cs | 10 ++ 8 files changed, 318 insertions(+), 297 deletions(-) delete mode 100644 src/Umbraco.Core/Factories/NavigationFactory.cs diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs index f0a9ef93e6..d2f3e0a6cd 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs @@ -19,6 +19,8 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase(), - StaticServiceProvider.Instance.GetRequiredService() + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService() ) { @@ -55,7 +59,9 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase descendants = _contentService.GetPagedDescendants(content.Id, 0, int.MaxValue, out _); + foreach (IContent descendant in content.Yield().Concat(descendants)) + { + HandleNavigationForSingleContent(descendant); + } + } + } + + private void HandleNavigationForSingleContent(IContent content) + { + // First creation + if (ExistsInNavigation(content.Key) is false && ExistsInNavigationBin(content.Key) is false) + { + _documentNavigationManagementService.Add(content.Key, GetParentKey(content)); + if (content.Trashed) + { + // If created as trashed, move to bin + _documentNavigationManagementService.MoveToBin(content.Key); + } + } + else if (ExistsInNavigation(content.Key) && ExistsInNavigationBin(content.Key) is false) + { + if (content.Trashed) + { + // It must have been trashed + _documentNavigationManagementService.MoveToBin(content.Key); + } + else + { + // It must have been saved. Check if parent is different + if (_documentNavigationQueryService.TryGetParentKey(content.Key, out var oldParentKey)) + { + Guid? newParentKey = GetParentKey(content); + if (oldParentKey != newParentKey) + { + _documentNavigationManagementService.Move(content.Key, newParentKey); + } + } + } + } + else if (ExistsInNavigation(content.Key) is false && ExistsInNavigationBin(content.Key)) + { + if (content.Trashed is false) + { + // It must have been restored + _documentNavigationManagementService.RestoreFromBin(content.Key, GetParentKey(content)); + } + } + } + + private Guid? GetParentKey(IContent content) => (content.ParentId == -1) ? null : _idKeyMap.GetKeyForId(content.ParentId, UmbracoObjectTypes.Document).Result; + + private bool ExistsInNavigation(Guid contentKey) => _documentNavigationQueryService.TryGetParentKey(contentKey, out _); + + private bool ExistsInNavigationBin(Guid contentKey) => _documentNavigationQueryService.TryGetParentKeyInBin(contentKey, out _); + private void HandleRouting(JsonPayload payload) { if(payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs index e42f8ce4f0..bcc8630e57 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Cache; @@ -13,6 +14,9 @@ namespace Umbraco.Cms.Core.Cache; public sealed class MediaCacheRefresher : PayloadCacheRefresherBase { private readonly IIdKeyMap _idKeyMap; + private readonly IMediaNavigationQueryService _mediaNavigationQueryService; + private readonly IMediaNavigationManagementService _mediaNavigationManagementService; + private readonly IMediaService _mediaService; private readonly IPublishedSnapshotService _publishedSnapshotService; public MediaCacheRefresher( @@ -21,11 +25,17 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase(payload.Id)); + mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Key)); + + // remove those that are in the branch + if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) + { + var pathid = "," + payload.Id + ","; + mediaCache.Result?.ClearOfType((_, v) => v.Path?.Contains(pathid) ?? false); + } } - // repository cache - // it *was* done for each pathId but really that does not make sense - // only need to do it for the current media - mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Key)); - - // remove those that are in the branch - if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) - { - var pathid = "," + payload.Id + ","; - mediaCache.Result?.ClearOfType((_, v) => v.Path?.Contains(pathid) ?? false); - } + HandleNavigation(payload); } _publishedSnapshotService.Notify(payloads, out var hasPublishedDataChanged); @@ -110,9 +120,107 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase descendants = _mediaService.GetPagedDescendants(media.Id, 0, int.MaxValue, out _); + foreach (IMedia descendant in media.Yield().Concat(descendants)) + { + HandleNavigationForSingleMedia(descendant); + } + } + } + + private void HandleNavigationForSingleMedia(IMedia media) + { + // First creation + if (ExistsInNavigation(media.Key) is false && ExistsInNavigationBin(media.Key) is false) + { + _mediaNavigationManagementService.Add(media.Key, GetParentKey(media)); + if (media.Trashed) + { + // If created as trashed, move to bin + _mediaNavigationManagementService.MoveToBin(media.Key); + } + } + else if (ExistsInNavigation(media.Key) && ExistsInNavigationBin(media.Key) is false) + { + if (media.Trashed) + { + // It must have been trashed + _mediaNavigationManagementService.MoveToBin(media.Key); + } + else + { + // It must have been saved. Check if parent is different + if (_mediaNavigationQueryService.TryGetParentKey(media.Key, out var oldParentKey)) + { + Guid? newParentKey = GetParentKey(media); + if (oldParentKey != newParentKey) + { + _mediaNavigationManagementService.Move(media.Key, newParentKey); + } + } + } + } + else if (ExistsInNavigation(media.Key) is false && ExistsInNavigationBin(media.Key)) + { + if (media.Trashed is false) + { + // It must have been restored + _mediaNavigationManagementService.RestoreFromBin(media.Key, GetParentKey(media)); + } + } + } + + private Guid? GetParentKey(IMedia media) => (media.ParentId == -1) ? null : _idKeyMap.GetKeyForId(media.ParentId, UmbracoObjectTypes.Media).Result; + + private bool ExistsInNavigation(Guid contentKey) => _mediaNavigationQueryService.TryGetParentKey(contentKey, out _); + + private bool ExistsInNavigationBin(Guid contentKey) => _mediaNavigationQueryService.TryGetParentKeyInBin(contentKey, out _); + + // these events should never trigger // everything should be JSON public override void RefreshAll() => throw new NotSupportedException(); diff --git a/src/Umbraco.Core/Factories/NavigationFactory.cs b/src/Umbraco.Core/Factories/NavigationFactory.cs deleted file mode 100644 index a95cbf68a5..0000000000 --- a/src/Umbraco.Core/Factories/NavigationFactory.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Concurrent; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Navigation; - -namespace Umbraco.Cms.Core.Factories; - -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. - public static void BuildNavigationDictionary(ConcurrentDictionary nodesStructure, IEnumerable entities) - { - var entityList = entities.ToList(); - Dictionary idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key); - - foreach (INavigationModel entity in entityList) - { - var node = new NavigationNode(entity.Key); - nodesStructure[entity.Key] = node; - - // We don't set the parent for items under root, it will stay null - if (entity.ParentId == -1) - { - continue; - } - - if (idToKeyMap.TryGetValue(entity.ParentId, out Guid parentKey) is false) - { - continue; - } - - // If the parent node exists in the nodesStructure, add the node to the parent's children (parent is set as well) - if (nodesStructure.TryGetValue(parentKey, out NavigationNode? parentNode)) - { - parentNode.AddChild(node); - } - } - } -} diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index d73528984a..223f0d9119 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -35,7 +35,6 @@ public class ContentService : RepositoryService, IContentService private readonly IShortStringHelper _shortStringHelper; private readonly ICultureImpactFactory _cultureImpactFactory; private readonly IUserIdKeyResolver _userIdKeyResolver; - private readonly IDocumentNavigationManagementService _documentNavigationManagementService; private readonly PropertyEditorCollection _propertyEditorCollection; private IQuery? _queryNotTrashed; @@ -55,7 +54,6 @@ public class ContentService : RepositoryService, IContentService IShortStringHelper shortStringHelper, ICultureImpactFactory cultureImpactFactory, IUserIdKeyResolver userIdKeyResolver, - IDocumentNavigationManagementService documentNavigationManagementService, PropertyEditorCollection propertyEditorCollection) : base(provider, loggerFactory, eventMessagesFactory) { @@ -69,7 +67,6 @@ public class ContentService : RepositoryService, IContentService _shortStringHelper = shortStringHelper; _cultureImpactFactory = cultureImpactFactory; _userIdKeyResolver = userIdKeyResolver; - _documentNavigationManagementService = documentNavigationManagementService; _propertyEditorCollection = propertyEditorCollection; _logger = loggerFactory.CreateLogger(); } @@ -103,7 +100,6 @@ public class ContentService : RepositoryService, IContentService shortStringHelper, cultureImpactFactory, userIdKeyResolver, - StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()) { } @@ -136,7 +132,6 @@ public class ContentService : RepositoryService, IContentService shortStringHelper, cultureImpactFactory, StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()) { } @@ -1063,18 +1058,6 @@ public class ContentService : RepositoryService, IContentService // have always changed if it's been saved in the back office but that's not really fail safe. _documentRepository.Save(content); - // Updates in-memory navigation structure - we only handle new items, other updates are not a concern - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.ContentService.Save-with-contentSchedule", - () => - { - _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key); - if (content.Trashed) - { - _documentNavigationManagementService.MoveToBin(content.Key); - } - }); - if (contentSchedule != null) { _documentRepository.PersistContentSchedule(content, contentSchedule); @@ -1138,18 +1121,6 @@ public class ContentService : RepositoryService, IContentService content.WriterId = userId; _documentRepository.Save(content); - - // Updates in-memory navigation structure - we only handle new items, other updates are not a concern - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.ContentService.Save", - () => - { - _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key); - if (content.Trashed) - { - _documentNavigationManagementService.MoveToBin(content.Key); - } - }); } scope.Notifications.Publish( @@ -2339,26 +2310,6 @@ public class ContentService : RepositoryService, IContentService } DoDelete(content); - - if (content.Trashed) - { - // Updates in-memory navigation structure for recycle bin items - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.ContentService.DeleteLocked-trashed", - () => _documentNavigationManagementService.RemoveFromBin(content.Key)); - } - else - { - // Updates in-memory navigation structure for both documents and recycle bin items - // as the item needs to be deleted whether it is in the recycle bin or not - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.ContentService.DeleteLocked", - () => - { - _documentNavigationManagementService.MoveToBin(content.Key); - _documentNavigationManagementService.RemoveFromBin(content.Key); - }); - } } // TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way @@ -2583,8 +2534,6 @@ public class ContentService : RepositoryService, IContentService // trash indicates whether we are trashing, un-trashing, or not changing anything private void PerformMoveLocked(IContent content, int parentId, IContent? parent, int userId, ICollection<(IContent, string)> moves, bool? trash) { - // Needed to update the in-memory navigation structure - var cameFromRecycleBin = content.ParentId == Constants.System.RecycleBinContent; content.WriterId = userId; content.ParentId = parentId; @@ -2633,33 +2582,6 @@ public class ContentService : RepositoryService, IContentService } } while (total > pageSize); - - if (parentId == Constants.System.RecycleBinContent) - { - // Updates in-memory navigation structure for both document items and recycle bin items - // as we are moving to recycle bin - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked-to-recycle-bin", - () => _documentNavigationManagementService.MoveToBin(content.Key)); - } - else - { - if (cameFromRecycleBin) - { - // Updates in-memory navigation structure for both document items and recycle bin items - // as we are restoring from recycle bin - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked-restore", - () => _documentNavigationManagementService.RestoreFromBin(content.Key, parent?.Key)); - } - else - { - // Updates in-memory navigation structure - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked", - () => _documentNavigationManagementService.Move(content.Key, parent?.Key)); - } - } } private void PerformMoveContentLocked(IContent content, int userId, bool? trash) @@ -2864,20 +2786,6 @@ public class ContentService : RepositoryService, IContentService } } - if (navigationUpdates.Count > 0) - { - // Updates in-memory navigation structure - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.ContentService.Copy", - () => - { - foreach (Tuple update in navigationUpdates) - { - _documentNavigationManagementService.Add(update.Item1, update.Item2); - } - }); - } - // not handling tags here, because // - tags should be handled by the content repository // - a copy is unpublished and therefore has no impact on tags in DB @@ -3822,28 +3730,4 @@ public class ContentService : RepositoryService, IContentService #endregion - /// - /// Enlists an action in the current scope context to update the in-memory navigation structure - /// when the scope completes successfully. - /// - /// The unique key identifying the action to be enlisted. - /// The action to be performed for updating the in-memory navigation structure. - /// Thrown when the scope context is null and therefore cannot be used. - private void UpdateInMemoryNavigationStructure(string enlistingActionKey, Action updateNavigation) - { - IScopeContext? scopeContext = ScopeProvider.Context; - - if (scopeContext is null) - { - throw new NullReferenceException($"The {nameof(scopeContext)} is null and cannot be used."); - } - - scopeContext.Enlist(enlistingActionKey, completed => - { - if (completed) - { - updateNavigation(); - } - }); - } } diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index ae334447e9..93431618b2 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -29,7 +29,6 @@ namespace Umbraco.Cms.Core.Services private readonly IEntityRepository _entityRepository; private readonly IShortStringHelper _shortStringHelper; private readonly IUserIdKeyResolver _userIdKeyResolver; - private readonly IMediaNavigationManagementService _mediaNavigationManagementService; private readonly MediaFileManager _mediaFileManager; @@ -45,8 +44,7 @@ namespace Umbraco.Cms.Core.Services IMediaTypeRepository mediaTypeRepository, IEntityRepository entityRepository, IShortStringHelper shortStringHelper, - IUserIdKeyResolver userIdKeyResolver, - IMediaNavigationManagementService mediaNavigationManagementService) + IUserIdKeyResolver userIdKeyResolver) : base(provider, loggerFactory, eventMessagesFactory) { _mediaFileManager = mediaFileManager; @@ -56,36 +54,7 @@ namespace Umbraco.Cms.Core.Services _entityRepository = entityRepository; _shortStringHelper = shortStringHelper; _userIdKeyResolver = userIdKeyResolver; - _mediaNavigationManagementService = mediaNavigationManagementService; } - - [Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")] - public MediaService( - ICoreScopeProvider provider, - MediaFileManager mediaFileManager, - ILoggerFactory loggerFactory, - IEventMessagesFactory eventMessagesFactory, - IMediaRepository mediaRepository, - IAuditRepository auditRepository, - IMediaTypeRepository mediaTypeRepository, - IEntityRepository entityRepository, - IShortStringHelper shortStringHelper, - IUserIdKeyResolver userIdKeyResolver) - : this( - provider, - mediaFileManager, - loggerFactory, - eventMessagesFactory, - mediaRepository, - auditRepository, - mediaTypeRepository, - entityRepository, - shortStringHelper, - userIdKeyResolver, - StaticServiceProvider.Instance.GetRequiredService()) - { - } - [Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")] public MediaService( ICoreScopeProvider provider, @@ -107,8 +76,7 @@ namespace Umbraco.Cms.Core.Services mediaTypeRepository, entityRepository, shortStringHelper, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -801,11 +769,6 @@ namespace Umbraco.Cms.Core.Services _mediaRepository.Save(media); - // Updates in-memory navigation structure - we only handle new items, other updates are not a concern - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.MediaService.Save", - () => _mediaNavigationManagementService.Add(media.Key, GetParent(media)?.Key)); - scope.Notifications.Publish(new MediaSavedNotification(media, eventMessages).WithStateFrom(savingNotification)); // TODO: See note about suppressing events in content service scope.Notifications.Publish(new MediaTreeChangeNotification(media, TreeChangeTypes.RefreshNode, eventMessages)); @@ -847,11 +810,6 @@ namespace Umbraco.Cms.Core.Services } _mediaRepository.Save(media); - - // Updates in-memory navigation structure - we only handle new items, other updates are not a concern - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.ContentService.Save-collection", - () => _mediaNavigationManagementService.Add(media.Key, GetParent(media)?.Key)); } scope.Notifications.Publish(new MediaSavedNotification(mediasA, messages).WithStateFrom(savingNotification)); @@ -923,26 +881,6 @@ namespace Umbraco.Cms.Core.Services } DoDelete(media); - - if (media.Trashed) - { - // Updates in-memory navigation structure for recycle bin items - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.MediaService.DeleteLocked-trashed", - () => _mediaNavigationManagementService.RemoveFromBin(media.Key)); - } - else - { - // Updates in-memory navigation structure for both media and recycle bin items - // as the item needs to be deleted whether it is in the recycle bin or not - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.MediaService.DeleteLocked", - () => - { - _mediaNavigationManagementService.MoveToBin(media.Key); - _mediaNavigationManagementService.RemoveFromBin(media.Key); - }); - } } //TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way @@ -1177,33 +1115,6 @@ namespace Umbraco.Cms.Core.Services } while (total > pageSize); - - if (parentId == Constants.System.RecycleBinMedia) - { - // Updates in-memory navigation structure for both media items and recycle bin items - // as we are moving to recycle bin - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked-to-recycle-bin", - () => _mediaNavigationManagementService.MoveToBin(media.Key)); - } - else - { - if (cameFromRecycleBin) - { - // Updates in-memory navigation structure for both media items and recycle bin items - // as we are restoring from recycle bin - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked-restore", - () => _mediaNavigationManagementService.RestoreFromBin(media.Key, parent?.Key)); - } - else - { - // Updates in-memory navigation structure - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked", - () => _mediaNavigationManagementService.Move(media.Key, parent?.Key)); - } - } } private void PerformMoveMediaLocked(IMedia media, bool? trash) @@ -1511,29 +1422,5 @@ namespace Umbraco.Cms.Core.Services #endregion - /// - /// Enlists an action in the current scope context to update the in-memory navigation structure - /// when the scope completes successfully. - /// - /// The unique key identifying the action to be enlisted. - /// The action to be performed for updating the in-memory navigation structure. - /// Thrown when the scope context is null and therefore cannot be used. - private void UpdateInMemoryNavigationStructure(string enlistingActionKey, Action updateNavigation) - { - IScopeContext? scopeContext = ScopeProvider.Context; - - if (scopeContext is null) - { - throw new NullReferenceException($"The {nameof(scopeContext)} is null and cannot be used."); - } - - scopeContext.Enlist(enlistingActionKey, completed => - { - if (completed) - { - updateNavigation(); - } - }); - } } } diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 4f2d1e1c9b..fb6c7a0381 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -13,6 +13,8 @@ internal abstract class ContentNavigationServiceBase private readonly INavigationRepository _navigationRepository; private ConcurrentDictionary _navigationStructure = new(); private ConcurrentDictionary _recycleBinNavigationStructure = new(); + private IList _roots = new List(); + private IList _recycleBinRoots = new List(); protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) { @@ -34,7 +36,7 @@ internal abstract class ContentNavigationServiceBase => TryGetParentKeyFromStructure(_navigationStructure, childKey, out parentKey); public bool TryGetRootKeys(out IEnumerable rootKeys) - => TryGetRootKeysFromStructure(_navigationStructure, out rootKeys); + => TryGetRootKeysFromStructure(_roots, out rootKeys); public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys) => TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys); @@ -87,6 +89,10 @@ internal abstract class ContentNavigationServiceBase return false; // Parent node doesn't exist } } + else + { + _roots.Add(key); + } var newNode = new NavigationNode(key); if (_navigationStructure.TryAdd(key, newNode) is false) @@ -111,10 +117,19 @@ internal abstract class ContentNavigationServiceBase return false; // Cannot move a node to itself } + _roots.Remove(key); // Just in case + NavigationNode? targetParentNode = null; - if (targetParentKey.HasValue && _navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false) + if (targetParentKey.HasValue) { - return false; // Target parent doesn't exist + if (_navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false) + { + return false; // Target parent doesn't exist + } + } + else + { + _roots.Add(key); } // Remove the node from its current parent's children list @@ -136,6 +151,8 @@ internal abstract class ContentNavigationServiceBase return false; // Node doesn't exist } + _recycleBinRoots.Remove(key); + RemoveDescendantsRecursively(nodeToRemove); return _recycleBinNavigationStructure.TryRemove(key, out _); @@ -158,6 +175,7 @@ internal abstract class ContentNavigationServiceBase // Set the new parent for the node (if parent node is null - the node is moved to root) targetParentNode?.AddChild(nodeToRestore); + // Restore the node and its descendants from the recycle bin to the main structure RestoreNodeAndDescendantsRecursively(nodeToRestore); @@ -187,12 +205,12 @@ internal abstract class ContentNavigationServiceBase if (trashed) { IEnumerable navigationModels = _navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey); - NavigationFactory.BuildNavigationDictionary(_recycleBinNavigationStructure, navigationModels); + BuildNavigationDictionary(_recycleBinNavigationStructure, _recycleBinRoots, navigationModels); } else { IEnumerable navigationModels = _navigationRepository.GetContentNodesByObjectType(objectTypeKey); - NavigationFactory.BuildNavigationDictionary(_navigationStructure, navigationModels); + BuildNavigationDictionary(_navigationStructure, _roots, navigationModels); } } @@ -209,10 +227,11 @@ internal abstract class ContentNavigationServiceBase return false; } - private bool TryGetRootKeysFromStructure(ConcurrentDictionary structure, out IEnumerable rootKeys) + private bool TryGetRootKeysFromStructure(IList input, out IEnumerable rootKeys) { // TODO can we make this more efficient? - rootKeys = structure.Values.Where(x => x.Parent is null).Select(x => x.Key); + rootKeys = input.ToArray(); + return true; } @@ -323,6 +342,9 @@ internal abstract class ContentNavigationServiceBase private void AddDescendantsToRecycleBinRecursively(NavigationNode node) { + _recycleBinRoots.Add(node.Key); + _roots.Remove(node.Key); + foreach (NavigationNode child in node.Children) { AddDescendantsToRecycleBinRecursively(child); @@ -346,6 +368,12 @@ internal abstract class ContentNavigationServiceBase private void RestoreNodeAndDescendantsRecursively(NavigationNode node) { + if (node.Parent is null) + { + _roots.Add(node.Key); + } + _recycleBinRoots.Remove(node.Key); + foreach (NavigationNode child in node.Children) { RestoreNodeAndDescendantsRecursively(child); @@ -357,4 +385,34 @@ internal abstract class ContentNavigationServiceBase } } } + + private static void BuildNavigationDictionary(ConcurrentDictionary nodesStructure, IList roots, IEnumerable entities) + { + var entityList = entities.ToList(); + IDictionary idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key); + + foreach (INavigationModel entity in entityList) + { + var node = new NavigationNode(entity.Key); + nodesStructure[entity.Key] = node; + + // We don't set the parent for items under root, it will stay null + if (entity.ParentId == -1) + { + roots.Add(entity.Key); + continue; + } + + if (idToKeyMap.TryGetValue(entity.ParentId, out Guid parentKey) is false) + { + continue; + } + + // If the parent node exists in the nodesStructure, add the node to the parent's children (parent is set as well) + if (nodesStructure.TryGetValue(parentKey, out NavigationNode? parentNode)) + { + parentNode.AddChild(node); + } + } + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs index d4325f4674..375ddb8391 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs @@ -1,11 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; @@ -48,4 +53,10 @@ public abstract class DocumentNavigationServiceTestsBase : UmbracoIntegrationTes InvariantName = name, Key = key, }; + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + builder.AddNotificationHandler(); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs index 6d9e693320..6952823b9c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs @@ -1,11 +1,15 @@ using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; @@ -48,4 +52,10 @@ public abstract class MediaNavigationServiceTestsBase : UmbracoIntegrationTest InvariantName = name, Key = key, }; + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + builder.AddNotificationHandler(); + } } From 7523e476da7d06f98d6118b24a127ae93fc2fd03 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 1 Oct 2024 07:29:04 +0200 Subject: [PATCH 12/25] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 5a13b7741d..5a1dac79a8 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 5a13b7741d681cf3d24e8ff5616b1000ade19f93 +Subproject commit 5a1dac79a8de17fd44651b980289606ed25800b1 From d3496ea5b01b9dd9993e4a09d3f7b03f4cdd5fc7 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:22:36 +0200 Subject: [PATCH 13/25] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 5a1dac79a8..edb33879cb 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 5a1dac79a8de17fd44651b980289606ed25800b1 +Subproject commit edb33879cb38fb6d62b0f192b331fe4783afa06d From 2f938e0c5a589397a496055828ba373a0d7a6e58 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Tue, 1 Oct 2024 10:40:36 +0100 Subject: [PATCH 14/25] Swaps TinyMCE for Tiptap as the default RTE This applies to new installations. Existing (upgraded) default RTE data-type will remain as TinyMCE. --- .../Migrations/Install/DatabaseDataCreator.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 53f7495822..d9eefbe76f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -195,10 +195,10 @@ internal class DatabaseDataCreator { var userGroupKeyToPermissions = new Dictionary>() { - [Constants.Security.AdminGroupKey] = new []{ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionAssignDomain.ActionLetter, ActionPublish.ActionLetter, ActionRights.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "7", "T"}, - [Constants.Security.WriterGroupKey] = new []{ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionToPublish.ActionLetter, ActionBrowse.ActionLetter, ActionNotify.ActionLetter, ":"}, - [Constants.Security.EditorGroupKey] = new []{ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionPublish.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "T"}, - [Constants.Security.TranslatorGroupKey] = new []{ActionUpdate.ActionLetter, ActionBrowse.ActionLetter}, + [Constants.Security.AdminGroupKey] = new[] { ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionAssignDomain.ActionLetter, ActionPublish.ActionLetter, ActionRights.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "7", "T" }, + [Constants.Security.WriterGroupKey] = new[] { ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionToPublish.ActionLetter, ActionBrowse.ActionLetter, ActionNotify.ActionLetter, ":" }, + [Constants.Security.EditorGroupKey] = new[] { ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionPublish.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "T" }, + [Constants.Security.TranslatorGroupKey] = new[] { ActionUpdate.ActionLetter, ActionBrowse.ActionLetter }, }; var i = 1; @@ -1921,7 +1921,7 @@ internal class DatabaseDataCreator }); } - if (_database.Exists(-87)) + if (_database.Exists(Constants.DataTypes.RichtextEditor)) { _database.Insert( Constants.DatabaseSchema.Tables.DataType, @@ -1929,12 +1929,11 @@ internal class DatabaseDataCreator false, new DataTypeDto { - NodeId = -87, + NodeId = Constants.DataTypes.RichtextEditor, EditorAlias = Constants.PropertyEditors.Aliases.RichText, - EditorUiAlias = "Umb.PropertyEditorUi.TinyMCE", + EditorUiAlias = "Umb.PropertyEditorUi.Tiptap", DbType = "Ntext", - Configuration = - "{\"toolbar\":[\"sourcecode\",\"styles\",\"bold\",\"italic\",\"alignleft\",\"aligncenter\",\"alignright\",\"bullist\",\"numlist\",\"outdent\",\"indent\",\"link\",\"umbmediapicker\",\"umbembeddialog\"],\"stylesheets\":[],\"maxImageSize\":500,\"mode\":\"classic\"}", + Configuration = "{\"extensions\": [\"Umb.Tiptap.Block\", \"Umb.Tiptap.Blockquote\", \"Umb.Tiptap.Bold\", \"Umb.Tiptap.CodeBlock\", \"Umb.Tiptap.Embed\", \"Umb.Tiptap.Figure\", \"Umb.Tiptap.Heading\", \"Umb.Tiptap.HorizontalRule\", \"Umb.Tiptap.Image\", \"Umb.Tiptap.Italic\", \"Umb.Tiptap.Link\", \"Umb.Tiptap.List\", \"Umb.Tiptap.MediaUpload\", \"Umb.Tiptap.Strike\", \"Umb.Tiptap.Subscript\", \"Umb.Tiptap.Superscript\", \"Umb.Tiptap.Table\", \"Umb.Tiptap.TextAlign\", \"Umb.Tiptap.Underline\"], \"maxImageSize\": 500, \"overlaySize\": \"medium\", \"toolbar\": [[[\"Umb.Tiptap.Toolbar.SourceEditor\"], [\"Umb.Tiptap.Toolbar.Bold\", \"Umb.Tiptap.Toolbar.Italic\", \"Umb.Tiptap.Toolbar.Underline\"], [\"Umb.Tiptap.Toolbar.TextAlignLeft\", \"Umb.Tiptap.Toolbar.TextAlignCenter\", \"Umb.Tiptap.Toolbar.TextAlignRight\"], [\"Umb.Tiptap.Toolbar.BulletList\", \"Umb.Tiptap.Toolbar.OrderedList\"], [\"Umb.Tiptap.Toolbar.Blockquote\", \"Umb.Tiptap.Toolbar.HorizontalRule\"], [\"Umb.Tiptap.Toolbar.Link\", \"Umb.Tiptap.Toolbar.Unlink\"], [\"Umb.Tiptap.Toolbar.MediaPicker\", \"Umb.Tiptap.Toolbar.EmbeddedMedia\"]]]}", }); } From 9117e09fd63ba994d2d0bdaf5d8b638e14b3d8b2 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:59:37 +0200 Subject: [PATCH 15/25] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index edb33879cb..776a4145c4 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit edb33879cb38fb6d62b0f192b331fe4783afa06d +Subproject commit 776a4145c4fbdd02ee796588107f9479ff9f6502 From 619a84ed6e69b7ee6ad33bdc84d37717823f1740 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 1 Oct 2024 14:09:25 +0200 Subject: [PATCH 16/25] Fixed issue with routing, where unpublished cultures still got a route. --- .../Services/DocumentUrlService.cs | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs index 2964314399..b1496aaec7 100644 --- a/src/Umbraco.Core/Services/DocumentUrlService.cs +++ b/src/Umbraco.Core/Services/DocumentUrlService.cs @@ -280,17 +280,26 @@ public class DocumentUrlService : IDocumentUrlService private IEnumerable GenerateModels(IContent document, string? culture, ILanguage language) { - var publishedUrlSegment = document.GetUrlSegment(_shortStringHelper, _urlSegmentProviderCollection, culture, true); - if(publishedUrlSegment.IsNullOrWhiteSpace()) + if (document.ContentType.VariesByCulture() is false || document.PublishCultureInfos != null && document.PublishCultureInfos.Values.Any(x => x.Culture == culture)) { - _logger.LogWarning("No published url segment found for document {DocumentKey} in culture {Culture}", document.Key, culture ?? "{null}"); - } - else - { - yield return new PublishedDocumentUrlSegment() + + var publishedUrlSegment = + document.GetUrlSegment(_shortStringHelper, _urlSegmentProviderCollection, culture, true); + if (publishedUrlSegment.IsNullOrWhiteSpace()) { - DocumentKey = document.Key, LanguageId = language.Id, UrlSegment = publishedUrlSegment, IsDraft = false - }; + _logger.LogWarning("No published url segment found for document {DocumentKey} in culture {Culture}", + document.Key, culture ?? "{null}"); + } + else + { + yield return new PublishedDocumentUrlSegment() + { + DocumentKey = document.Key, + LanguageId = language.Id, + UrlSegment = publishedUrlSegment, + IsDraft = false + }; + } } var draftUrlSegment = document.GetUrlSegment(_shortStringHelper, _urlSegmentProviderCollection, culture, false); From 2e563f70f3b6f6e48e1d1525a38809226e9dde49 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 1 Oct 2024 14:22:49 +0200 Subject: [PATCH 17/25] Make RTE work without blocks in a culture variant context (#17163) --- .../PropertyEditors/RichTextPropertyEditor.cs | 9 +-- .../RichTextElementLevelVariationTests.cs | 74 ++++++++++++++++++- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index b28a99472b..31cabac3bf 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -270,21 +270,20 @@ public class RichTextPropertyEditor : DataEditor internal override object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) { - if (sourceValue is null) + if (sourceValue is null || TryParseEditorValue(sourceValue, out RichTextEditorValue? sourceRichTextEditorValue) is false) { return null; } - if (TryParseEditorValue(sourceValue, out RichTextEditorValue? sourceRichTextEditorValue) is false - || sourceRichTextEditorValue.Blocks is null) + if (sourceRichTextEditorValue.Blocks is null) { - return null; + return sourceValue; } BlockEditorData? sourceBlockEditorData = ConvertAndClean(sourceRichTextEditorValue.Blocks); if (sourceBlockEditorData?.Layout is null) { - return null; + return sourceValue; } TryParseEditorValue(targetValue, out RichTextEditorValue? targetRichTextEditorValue); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs index 934ceea083..ad40548aac 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; @@ -363,6 +364,64 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes } } + [Test] + public async Task Can_Publish_Without_Blocks_Variant() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var blockGridDataType = await CreateRichTextDataType(elementType); + var contentType = CreateContentType(blockGridDataType); + var richTextValue = new RichTextEditorValue { Markup = "

Markup here

", Blocks = null }; + var content = CreateContent(contentType, richTextValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US"); + AssertPropertyValues("da-DK"); + + void AssertPropertyValues(string culture) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + var property = publishedContent.GetProperty("blocks"); + Assert.IsNotNull(property); + + var propertyValue = property.GetDeliveryApiValue(false, culture) as RichTextModel; + Assert.IsNotNull(propertyValue); + Assert.AreEqual("

Markup here

", propertyValue.Markup); + Assert.IsEmpty(propertyValue.Blocks); + } + } + + [Test] + public async Task Can_Publish_Without_Blocks_Invariant() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var blockGridDataType = await CreateRichTextDataType(elementType); + var contentType = CreateContentType(ContentVariation.Nothing, blockGridDataType); + var richTextValue = new RichTextEditorValue { Markup = "

Markup here

", Blocks = null }; + var content = CreateContent(contentType, richTextValue); + + PublishContent(content, ["*"]); + + AssertPropertyValues("en-US"); + AssertPropertyValues("da-DK"); + + void AssertPropertyValues(string culture) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + var property = publishedContent.GetProperty("blocks"); + Assert.IsNotNull(property); + + var propertyValue = property.GetDeliveryApiValue(false, culture) as RichTextModel; + Assert.IsNotNull(propertyValue); + Assert.AreEqual("

Markup here

", propertyValue.Markup); + Assert.IsEmpty(propertyValue.Blocks); + } + } + private async Task CreateRichTextDataType(IContentType elementType) => await CreateBlockEditorDataType( Constants.PropertyEditors.Aliases.RichText, @@ -480,9 +539,18 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes private IContent CreateContent(IContentType contentType, RichTextEditorValue richTextValue) { var contentBuilder = new ContentBuilder() - .WithContentType(contentType) - .WithCultureName("en-US", "Home (en)") - .WithCultureName("da-DK", "Home (da)"); + .WithContentType(contentType); + + if (contentType.VariesByCulture()) + { + contentBuilder + .WithCultureName("en-US", "Home (en)") + .WithCultureName("da-DK", "Home (da)"); + } + else + { + contentBuilder.WithName("Home"); + } var content = contentBuilder.Build(); From 31347dca742c6695a7a7e629a379b5afc01d51c7 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:32:34 +0200 Subject: [PATCH 18/25] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 776a4145c4..780b7e8e20 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 776a4145c4fbdd02ee796588107f9479ff9f6502 +Subproject commit 780b7e8e20da89db5f664d8c8cc10d9667cbb08c From 1cf7d7ad2b9d191e6b0f565de65a379b3cae2b5a Mon Sep 17 00:00:00 2001 From: kjac Date: Tue, 1 Oct 2024 14:38:51 +0200 Subject: [PATCH 19/25] Fix document URL migration (split it in two) --- src/Umbraco.Core/Services/DocumentUrlService.cs | 2 +- .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../Upgrade/V_15_0_0/AddDocumentUrl.cs | 13 +++---------- .../Upgrade/V_15_0_0/RebuildDocumentUrls.cs | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/RebuildDocumentUrls.cs diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs index b1496aaec7..1b69029fd4 100644 --- a/src/Umbraco.Core/Services/DocumentUrlService.cs +++ b/src/Umbraco.Core/Services/DocumentUrlService.cs @@ -170,7 +170,7 @@ public class DocumentUrlService : IDocumentUrlService using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); scope.ReadLock(Constants.Locks.ContentTree); - IEnumerable documents = _documentRepository.GetMany(Array.Empty()); + IEnumerable documents = _documentRepository.GetMany(Array.Empty()); await CreateOrUpdateUrlSegmentsAsync(documents); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 459af0304e..5b09ef3fc2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -99,6 +99,7 @@ public class UmbracoPlan : MigrationPlan // To 15.0.0 To("{7F4F31D8-DD71-4F0D-93FC-2690A924D84B}"); To("{1A8835EF-F8AB-4472-B4D8-D75B7C164022}"); + To("{3FE0FA2D-CF4F-4892-BA8D-E97D06E028DC}"); To("{6C04B137-0097-4938-8C6A-276DF1A0ECA8}"); To("{9D3CE7D4-4884-41D4-98E8-302EB6CB0CF6}"); To("{37875E80-5CDD-42FF-A21A-7D4E3E23E0ED}"); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddDocumentUrl.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddDocumentUrl.cs index 84c158eb33..d1b70f034e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddDocumentUrl.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddDocumentUrl.cs @@ -1,22 +1,15 @@ -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; [Obsolete("Remove in Umbraco 18.")] public class AddDocumentUrl : MigrationBase { - private readonly IDocumentUrlService _documentUrlService; - - public AddDocumentUrl(IMigrationContext context, IDocumentUrlService documentUrlService) + public AddDocumentUrl(IMigrationContext context) : base(context) { - _documentUrlService = documentUrlService; } protected override void Migrate() - { - Create.Table().Do(); - _documentUrlService.InitAsync(false, CancellationToken.None); - } + => Create.Table().Do(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/RebuildDocumentUrls.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/RebuildDocumentUrls.cs new file mode 100644 index 0000000000..79b9a2596c --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/RebuildDocumentUrls.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; + +[Obsolete("Remove in Umbraco 18.")] +public class RebuildDocumentUrls : MigrationBase +{ + private readonly IDocumentUrlService _documentUrlService; + + public RebuildDocumentUrls(IMigrationContext context, IDocumentUrlService documentUrlService) + : base(context) => + _documentUrlService = documentUrlService; + + protected override void Migrate() + => _documentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); +} From 7ca96423f83badd8a97c982e3e1c35f66681729a Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:52:37 +0200 Subject: [PATCH 20/25] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 780b7e8e20..239df086ad 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 780b7e8e20da89db5f664d8c8cc10d9667cbb08c +Subproject commit 239df086adb01be2b70861e01a42a19738ae775f From 1258962429e47828e620a3bd860d808946521896 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 1 Oct 2024 15:03:02 +0200 Subject: [PATCH 21/25] V15: Remove Nucache (#17166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove nucache reference from Web.Common * Get tests building-ish * Move ReservedFieldNamesService to the right project * Remove IPublishedSnapshotStatus * Added functionality to the INavigationQueryService to get root keys * Fixed issue with navigation * Remove IPublishedSnapshot from UmbracoContext * Begin removing usage of IPublishedSnapshot from PublishedContentExtensions * Fix PublishedContentExtensions.cs * Don't use snapshots in delivery media api * Use IPublishedMediaCache in QueryMediaApiController * Remove more usages of IPublishedSnapshotAccessor * Comment out tests * Remove more usages of PublishedSnapshotAccessor * Remove PublishedSnapshot from property * Fixed test build * Fix errors * Fix some tests * Delete NuCache 🎉 * Implement DatabaseCacheRebuilder * Remove usage of IPublishedSnapshotService * Remove IPublishedSnapshotService * Remove TestPublishedSnapshotAccessor and make tests build * Don't test Snapshot cachelevel It's no longer supported * Fix BlockEditorConverter Element != Element document type * Remember to set cachemanager * Fix RichTextParserTests * Implement TryGetLevel on INavigationQueryService * Fake level and obsolete it in PublishedContent * Remove ChildrenForAllCultures * Hack Path property on PublishedContent * Remove usages of IPublishedSnapshot in tests * More ConvertersTests * Add hybrid cache to integration tests We can actually do this now because we no longer save files on disk * Rename IPublishedSnapshotRebuilder to ICacheRebuilder * Comment out tests * V15: Replacing the usages of Parent (navigation data) from IPublishedContent (#17125) * Fix .Parent references in PublishedContentExtensions * Add missing methods to FriendlyPublishedContentExtensions (ones that you were able to call on the content directly as they now require extra params) * Fix references from the extension methods * Fix dependencies in tests * Replace IPublishedSnapshotAccessor with the content cache in tests * Resolving more .Parent references * Fix unit tests * Obsolete and use extension methods * Remove private method and use extension instead * Moving code around * Fix tests * Fix more references * Cleanup * Fix more usages * Resolve merge conflict * Fix tests * Cleanup * Fix more tests * Fixed unit tests * Cleanup * Replace last usages --------- Co-authored-by: Bjarke Berg * Remove usage of IPublishedSnapshotAccessor from IRequestItemProvider * Post merge fixup * Remo IPublishedSnapshot * Add HasAny to IDocumentUrlService * Fix TextBuilder * Fix modelsbuilder tests * Use explicit types * Implement GetByContentType * Support element types in PublishedContentTypeCache * Run enlistments before publishing notifications * Fix elements cache refreshing * Implement GetByUdi * Implement GetAtRoot * Implement GetByRoute * Reimplement GetRouteById * Fix blocks unit tests * Initialize domain cache on boot * Only return routes with domains on non default lanauges * V15: Replacing the usages of `Children` (navigation data) from `IPublishedContent` (#17159) * Update params in PublishedContentExtensions to the general interfaces for the published cache and navigation service, so that we can use the extension methods on both documents and media * Introduce GetParent() which uses the right services * Fix obsolete message on .Parent * Obsolete .Children * Fix usages of Children for ApiMediaQueryService * Fix usage in internal * Fix usages in views * Fix indentation * Fix issue with delete language * Update nuget pacakges * Clear elements cache when content is deleted instead of trying to update it * Reset publishedModelFactory * Fixed publishing --------- Co-authored-by: Bjarke Berg Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Co-authored-by: kjac --- Directory.Packages.props | 11 +- .../Media/ByIdMediaApiController.cs | 6 +- .../Media/ByIdsMediaApiController.cs | 4 +- .../Media/ByPathMediaApiController.cs | 4 +- .../Media/MediaApiControllerBase.cs | 11 +- .../Media/QueryMediaApiController.cs | 4 +- .../Querying/QueryOptionBase.cs | 11 +- .../Querying/Selectors/AncestorsSelector.cs | 30 +- .../Querying/Selectors/ChildrenSelector.cs | 4 +- .../Querying/Selectors/DescendantsSelector.cs | 4 +- .../Services/ApiMediaQueryService.cs | 16 +- .../Services/RequestRedirectService.cs | 4 +- .../Services/RequestRoutingService.cs | 5 +- .../Services/RequestStartItemProvider.cs | 25 +- .../Services/RoutingServiceBase.cs | 16 +- .../CollectPublishedCacheController.cs | 8 +- .../RebuildPublishedCacheController.cs | 7 +- .../StatusPublishedCacheController.cs | 8 +- .../Query/ExecuteTemplateQueryController.cs | 15 +- .../UmbracoBuilder.BackOffice.cs | 1 - .../Factories/DocumentUrlFactory.cs | 8 +- src/Umbraco.Core/Cache/CacheKeys.cs | 3 + .../Implement/ContentCacheRefresher.cs | 66 +- .../Implement/ContentTypeCacheRefresher.cs | 17 +- .../Implement/DataTypeCacheRefresher.cs | 16 +- .../Implement/DomainCacheRefresher.cs | 6 - .../Implement/LanguageCacheRefresher.cs | 15 +- .../Implement/MediaCacheRefresher.cs | 10 +- .../Composing/CompositionExtensions.cs | 45 - .../DeliveryApi/ApiContentRouteBuilder.cs | 57 +- .../DependencyInjection/UmbracoBuilder.cs | 3 - .../ListDescendantsFromCurrentPage.cshtml | 19 +- .../EmbeddedResources/Snippets/SiteMap.cshtml | 10 +- .../Extensions/PublishedContentExtensions.cs | 744 ++++- .../PublishedSnapshotAccessorExtensions.cs | 17 - .../Models/ContentRepositoryExtensions.cs | 2 +- .../PublishedContent/IPublishedContent.cs | 7 +- .../PublishedContent/PublishedContentBase.cs | 35 +- .../PublishedContentWrapped.cs | 10 +- .../PublishedValueFallback.cs | 6 +- .../MemberPickerValueConverter.cs | 14 +- .../MultiNodeTreePickerValueConverter.cs | 28 +- .../PublishedCache/IDatabaseCacheRebuilder.cs | 6 + .../PublishedCache/IPublishedCache.cs | 10 +- .../PublishedCache/IPublishedSnapshot.cs | 67 - .../IPublishedSnapshotAccessor.cs | 12 - .../IPublishedSnapshotService.cs | 132 - .../IPublishedSnapshotStatus.cs | 12 - .../Internal/InternalPublishedContent.cs | 14 +- .../Internal/InternalPublishedContentCache.cs | 63 - .../Internal/InternalPublishedSnapshot.cs | 36 - .../InternalPublishedSnapshotService.cs | 55 - .../PublishedCache/PublishedCacheBase.cs | 8 +- .../PublishedCache/PublishedElement.cs | 11 +- .../PublishedElementPropertyBase.cs | 42 +- ...UmbracoContextPublishedSnapshotAccessor.cs | 45 - src/Umbraco.Core/Routing/AliasUrlProvider.cs | 36 +- .../Routing/ContentFinderByUrlAlias.cs | 13 +- .../Routing/DefaultUrlProvider.cs | 64 +- .../Routing/NewDefaultUrlProvider.cs | 52 +- src/Umbraco.Core/Routing/PublishedRouter.cs | 4 +- src/Umbraco.Core/Routing/UrlProvider.cs | 38 +- .../Routing/UrlProviderExtensions.cs | 30 +- .../Services/DocumentUrlService.cs | 17 +- .../Services/IDocumentUrlService.cs | 2 + .../ContentNavigationServiceBase.cs | 22 + .../Navigation/INavigationQueryService.cs | 2 + src/Umbraco.Core/Web/IUmbracoContext.cs | 14 +- .../ContentEmptiedRecycleBinWebhookEvent.cs | 3 - .../Content/ContentPublishedWebhookEvent.cs | 15 +- .../Events/Content/ContentRolledBack.cs | 13 +- .../Content/ContentSavedWebhookEvent.cs | 15 +- .../Content/ContentSortedWebhookEvent.cs | 12 +- .../Events/Media/MediaSavedWebhookEvent.cs | 17 +- .../ApiMediaWithCropsResponseBuilder.cs | 30 +- .../DeliveryApi/ApiRichTextElementParser.cs | 46 +- .../DeliveryApi/ApiRichTextMarkupParser.cs | 23 +- .../DeliveryApi/ApiRichTextParserBase.cs | 29 +- .../UmbracoBuilder.CoreServices.cs | 12 +- .../UmbracoBuilder.Services.cs | 3 + .../Examine/ExamineExtensions.cs | 12 +- .../Migrations/MigrationPlanExecutor.cs | 49 +- .../PostMigrations/CacheRebuilder.cs | 32 + ...napshotRebuilder.cs => ICacheRebuilder.cs} | 2 +- .../PublishedSnapshotRebuilder.cs | 32 - .../ModelsBuilder/Building/TextBuilder.cs | 8 +- .../ModelsBuilder/PublishedModelUtility.cs | 10 +- .../Implement/LanguageRepository.cs | 1 + .../ValueConverters/BlockEditorConverter.cs | 20 +- .../MediaPickerWithCropsValueConverter.cs | 10 +- .../MultiUrlPickerValueConverter.cs | 26 +- .../PublishedContentTypeCache.cs | 4 + .../ReservedFieldNamesService.cs | 4 +- .../PublishedContentQuery.cs | 41 +- .../Routing/ContentFinderByConfigured404.cs | 4 +- .../Routing/RedirectTracker.cs | 19 +- src/Umbraco.Infrastructure/Scoping/Scope.cs | 2 +- .../Security/MemberUserStore.cs | 40 +- .../DatabaseCacheRebuilder.cs | 16 + .../UmbracoBuilderExtensions.cs | 4 +- .../DocumentCache.cs | 90 +- .../Factories/CacheNodeFactory.cs | 57 +- .../CacheRefreshingNotificationHandler.cs | 45 +- .../PublishedContent.cs | 57 +- .../PublishedProperty.cs | 4 +- .../Services/DocumentCacheService.cs | 15 +- .../Services/DomainCacheService.cs | 35 +- .../Services/IDocumentCacheService.cs | 2 + .../Umbraco.PublishedCache.HybridCache.csproj | 6 + .../CacheKeys.cs | 91 - .../ContentCache.cs | 371 --- .../ContentNode.cs | 212 -- .../ContentNodeKit.cs | 61 - .../ContentStore.cs | 2077 ------------- .../DataSource/BTree.ContentDataSerializer.cs | 53 - .../BTree.ContentNodeKitSerializer.cs | 83 - ....DictionaryOfCultureVariationSerializer.cs | 59 - ...Tree.DictionaryOfPropertyDataSerializer.cs | 84 - .../DataSource/BTree.cs | 86 - .../DataSource/ContentCacheDataModel.cs | 46 - .../ContentCacheDataSerializationResult.cs | 47 - .../ContentCacheDataSerializerEntityType.cs | 9 - .../DataSource/ContentData.cs | 36 - .../DataSource/ContentSourceDto.cs | 67 - .../DataSource/CultureVariation.cs | 47 - .../DataSource/IContentCacheDataSerializer.cs | 24 - .../IContentCacheDataSerializerFactory.cs | 15 - .../IDictionaryOfPropertyDataSerializer.cs | 8 - .../JsonContentNestedDataSerializer.cs | 39 - .../JsonContentNestedDataSerializerFactory.cs | 8 - .../DataSource/LazyCompressedString.cs | 105 - ...ctionaryStringInternIgnoreCaseFormatter.cs | 27 - .../MsgPackContentNestedDataSerializer.cs | 147 - ...gPackContentNestedDataSerializerFactory.cs | 69 - .../DataSource/PropertyData.cs | 62 - .../DataSource/SerializerBase.cs | 249 -- .../UmbracoBuilderExtensions.cs | 114 - .../DomainCache.cs | 55 - .../DomainCacheExtensions.cs | 19 - .../MediaCache.cs | 108 - .../MemberCache.cs | 70 - .../Navigable/INavigableContent.cs | 63 - .../Navigable/INavigableContentType.cs | 19 - .../Navigable/INavigableData.cs | 10 - .../Navigable/INavigableFieldType.cs | 23 - .../Navigable/INavigableSource.cs | 31 - .../Navigable/NavigableContent.cs | 69 - .../Navigable/NavigableContentType.cs | 64 - .../Navigable/NavigablePropertyType.cs | 14 - .../Navigable/RootContent.cs | 29 - .../Navigable/Source.cs | 30 - .../NuCacheStartupHandler.cs | 32 - .../Persistence/INuCacheContentRepository.cs | 65 - .../Persistence/INuCacheContentService.cs | 113 - .../Persistence/NuCacheContentRepository.cs | 1053 ------- .../Persistence/NuCacheContentService.cs | 183 -- .../Property.cs | 423 --- .../PublishedContent.cs | 438 --- .../PublishedMember.cs | 123 - .../PublishedSnapshot.cs | 126 - .../PublishedSnapshotService.cs | 1245 -------- .../PublishedSnapshotServiceEventHandler.cs | 126 - .../PublishedSnapshotServiceOptions.cs | 32 - .../PublishedSnapshotStatus.cs | 58 - .../Snap/GenObj.cs | 30 - .../Snap/GenRef.cs | 10 - .../Snap/LinkedNode.cs | 25 - .../SnapDictionary.cs | 677 ----- .../Umbraco.PublishedCache.NuCache.csproj | 39 - src/Umbraco.PublishedCache.NuCache/readme.md | 120 - .../UmbracoBuilder.MembersIdentity.cs | 4 +- .../FriendlyPublishedContentExtensions.cs | 198 +- .../Umbraco.Web.Common.csproj | 3 +- .../UmbracoContext/UmbracoContext.cs | 53 +- .../UmbracoContext/UmbracoContextFactory.cs | 34 +- .../Controllers/UmbLoginController.cs | 39 +- .../Routing/PublicAccessRequestHandler.cs | 2 +- .../Extensions/UmbracoBuilderExtensions.cs | 6 - .../Builders/ContentDataBuilder.cs | 7 +- .../Builders/ContentNodeKitBuilder.cs | 190 +- .../Builders/PropertyDataBuilder.cs | 59 +- .../Published/PublishedContentXmlAdapter.cs | 297 +- .../TestPublishedSnapshotAccessor.cs | 19 - .../UmbracoBuilderExtensions.cs | 3 - .../UmbracoTestServerTestBase.cs | 1 - .../Cache/PublishedContentTypeCacheTests.cs | 1 - .../Umbraco.Core/DeliveryApi/CacheTests.cs | 153 +- .../PublishedContentQueryTests.cs | 3 +- .../Migrations/AdvancedMigrationTests.cs | 4 +- .../BlockEditorElementVariationTestBase.cs | 37 +- .../Scoping/ScopedNuCacheTests.cs | 1 - .../Scoping/ScopedRepositoryTests.cs | 9 +- .../Services/CacheInstructionServiceTests.cs | 6 - .../Services/ContentEventsTests.cs | 2 +- .../ContentTypeServiceVariantsTests.cs | 2 +- .../Services/MemberServiceTests.cs | 21 +- .../Services/NuCacheRebuildTests.cs | 177 +- .../Services/TrackRelationsTests.cs | 10 +- .../DocumentHybridCacheDocumentTypeTests.cs | 1 - .../DocumentHybridCacheMockTests.cs | 1 - .../DocumentHybridCachePropertyTest.cs | 1 - .../DocumentHybridCacheScopeTests.cs | 1 - .../DocumentHybridCacheTemplateTests.cs | 87 +- .../DocumentHybridCacheTests.cs | 989 +++--- .../DocumentHybridCacheVariantsTests.cs | 1 - .../MediaHybridCacheTests.cs | 1 - .../MemberHybridCacheTests.cs | 1 - .../UrlAndDomains/DomainAndUrlsTests.cs | 8 +- .../Objects/TestUmbracoContextFactory.cs | 12 +- .../PublishedSnapshotServiceTestBase.cs | 591 ++-- .../TestHelpers/TestNuCacheContentService.cs | 217 +- .../Umbraco.Core/DeliveryApi/CacheTests.cs | 4 +- .../DeliveryApi/ContentBuilderTests.cs | 4 +- .../ContentPickerValueConverterTests.cs | 4 +- .../DeliveryApi/ContentRouteBuilderTests.cs | 202 +- .../DeliveryApi/DeliveryApiTests.cs | 12 +- ...MediaPickerWithCropsValueConverterTests.cs | 2 +- .../MultiNodeTreePickerValueConverterTests.cs | 10 +- .../MultiUrlPickerValueConverterTests.cs | 6 +- .../OutputExpansionStrategyTestBase.cs | 20 +- .../PropertyValueConverterTests.cs | 16 +- .../DeliveryApi/PublishedContentCacheTests.cs | 12 +- .../DeliveryApi/RichTextParserTests.cs | 30 +- .../BlockGridPropertyValueConverterTests.cs | 4 +- .../BlockListPropertyValueConverterTests.cs | 4 +- .../BlockPropertyValueConverterTestsBase.cs | 23 +- .../PropertyEditors/ConvertersTests.cs | 19 +- .../Umbraco.Core/Published/ConvertersTests.cs | 24 +- .../Published/PropertyCacheLevelTests.cs | 57 +- .../Published/PropertyCacheVarianceTests.cs | 763 ++--- .../PublishedContentVarianceTests.cs | 349 +-- .../Routing/ContentFinderByAliasTests.cs | 75 +- .../ContentFinderByAliasWithDomainsTests.cs | 117 +- .../Routing/ContentFinderByIdTests.cs | 101 +- .../ContentFinderByIdentifierTestsBase.cs | 117 +- .../Routing/ContentFinderByKeyTests.cs | 105 +- .../ContentFinderByPageIdQueryTests.cs | 121 +- .../ContentFinderByUrlAndTemplateTests.cs | 167 +- .../Routing/ContentFinderByUrlTests.cs | 329 +- .../ContentFinderByUrlWithDomainsTests.cs | 513 ++-- .../Routing/DomainsAndCulturesTests.cs | 719 ++--- .../Routing/GetContentUrlsTests.cs | 399 +-- .../Routing/PublishedRouterTests.cs | 1 - ...oviderWithHideTopLevelNodeFromPathTests.cs | 103 +- ...derWithoutHideTopLevelNodeFromPathTests.cs | 621 ++-- .../Routing/UrlRoutingTestBase.cs | 405 +-- .../Routing/UrlsProviderWithDomainsTests.cs | 967 +++--- .../Routing/UrlsWithNestedDomains.cs | 481 +-- .../Services/ContentNavigationServiceTest.cs | 100 + .../Templates/HtmlImageSourceParserTests.cs | 7 +- .../Templates/HtmlLocalLinkParserTests.cs | 23 +- .../ApiRichTextMarkupParserTests.cs | 16 +- .../Migrations/MigrationPlanTests.cs | 2 +- .../ContentSerializationTests.cs | 209 +- .../PublishedContentCacheTests.cs | 149 +- .../PublishedContentDataTableTests.cs | 391 +-- .../PublishedContentExtensionTests.cs | 153 +- .../PublishedContentLanguageVariantTests.cs | 753 ++--- .../PublishedCache/PublishedContentTests.cs | 1941 ++++++------ .../PublishedCache/PublishedMediaTests.cs | 483 +-- ...PublishedSnapshotServiceCollectionTests.cs | 2691 +++++++++-------- .../PublishedSnapshotServiceContentTests.cs | 413 +-- .../PublishedCache/RootNodeTests.cs | 97 +- .../PublishedCache/UrlRoutesTests.cs | 731 ++--- .../Security/MemberManagerTests.cs | 4 +- .../Security/MemberUserStoreTests.cs | 4 +- .../BuilderTests.cs | 48 +- .../SnapDictionaryTests.cs | 2361 +++++++-------- .../Controllers/SurfaceControllerTests.cs | 2 - umbraco.sln | 8 - 270 files changed, 12051 insertions(+), 21515 deletions(-) delete mode 100644 src/Umbraco.Core/Composing/CompositionExtensions.cs delete mode 100644 src/Umbraco.Core/Extensions/PublishedSnapshotAccessorExtensions.cs create mode 100644 src/Umbraco.Core/PublishedCache/IDatabaseCacheRebuilder.cs delete mode 100644 src/Umbraco.Core/PublishedCache/IPublishedSnapshot.cs delete mode 100644 src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs delete mode 100644 src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs delete mode 100644 src/Umbraco.Core/PublishedCache/IPublishedSnapshotStatus.cs delete mode 100644 src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs delete mode 100644 src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs delete mode 100644 src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs delete mode 100644 src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/PostMigrations/CacheRebuilder.cs rename src/Umbraco.Infrastructure/Migrations/PostMigrations/{IPublishedSnapshotRebuilder.cs => ICacheRebuilder.cs} (91%) delete mode 100644 src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs rename src/{Umbraco.PublishedCache.NuCache => Umbraco.Infrastructure/PublishedCache}/ReservedFieldNamesService.cs (90%) create mode 100644 src/Umbraco.PublishedCache.HybridCache/DatabaseCacheRebuilder.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/CacheKeys.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/ContentCache.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/ContentNode.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/ContentNodeKit.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/ContentStore.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentNodeKitSerializer.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataModel.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializationResult.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializerEntityType.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/ContentData.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializerFactory.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/LazyCompressedString.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/MessagePackDictionaryStringInternIgnoreCaseFormatter.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/SerializerBase.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DomainCache.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DomainCacheExtensions.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/MediaCache.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/MemberCache.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Navigable/INavigableContent.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Navigable/INavigableContentType.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Navigable/INavigableData.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Navigable/INavigableFieldType.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Navigable/INavigableSource.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Navigable/NavigableContent.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Navigable/NavigableContentType.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Navigable/NavigablePropertyType.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Navigable/RootContent.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Navigable/Source.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/NuCacheStartupHandler.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentRepository.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Property.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/PublishedContent.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/PublishedMember.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/PublishedSnapshot.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceOptions.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/PublishedSnapshotStatus.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Snap/GenObj.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Snap/GenRef.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Snap/LinkedNode.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/SnapDictionary.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj delete mode 100644 src/Umbraco.PublishedCache.NuCache/readme.md delete mode 100644 tests/Umbraco.Tests.Common/TestPublishedSnapshotAccessor.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceTest.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index e60b2c718a..8bedbf588a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -47,10 +47,10 @@ - + - + @@ -61,7 +61,7 @@ - + @@ -75,12 +75,12 @@ - + - + @@ -91,7 +91,6 @@ - diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdMediaApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdMediaApiController.cs index 76ce80898f..f7fa2f6d92 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdMediaApiController.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdMediaApiController.cs @@ -12,8 +12,10 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Media; [ApiVersion("2.0")] public class ByIdMediaApiController : MediaApiControllerBase { - public ByIdMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder) - : base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder) + public ByIdMediaApiController( + IPublishedMediaCache publishedMediaCache, + IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder) + : base(publishedMediaCache, apiMediaWithCropsResponseBuilder) { } diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdsMediaApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdsMediaApiController.cs index 8a3f4c7ceb..957c982c38 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdsMediaApiController.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdsMediaApiController.cs @@ -13,8 +13,8 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Media; [ApiVersion("2.0")] public class ByIdsMediaApiController : MediaApiControllerBase { - public ByIdsMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder) - : base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder) + public ByIdsMediaApiController(IPublishedMediaCache publishedMediaCache, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder) + : base(publishedMediaCache, apiMediaWithCropsResponseBuilder) { } diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByPathMediaApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByPathMediaApiController.cs index ed5dc90187..0afedddffb 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByPathMediaApiController.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByPathMediaApiController.cs @@ -16,10 +16,10 @@ public class ByPathMediaApiController : MediaApiControllerBase private readonly IApiMediaQueryService _apiMediaQueryService; public ByPathMediaApiController( - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedMediaCache publishedMediaCache, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder, IApiMediaQueryService apiMediaQueryService) - : base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder) + : base(publishedMediaCache, apiMediaWithCropsResponseBuilder) => _apiMediaQueryService = apiMediaQueryService; [HttpGet("item/{*path}")] diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/MediaApiControllerBase.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/MediaApiControllerBase.cs index 5a9bc4763e..7807290cc4 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/MediaApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/MediaApiControllerBase.cs @@ -20,18 +20,15 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Media; public abstract class MediaApiControllerBase : DeliveryApiControllerBase { private readonly IApiMediaWithCropsResponseBuilder _apiMediaWithCropsResponseBuilder; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private IPublishedMediaCache? _publishedMediaCache; + private IPublishedMediaCache _publishedMediaCache; - protected MediaApiControllerBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder) + protected MediaApiControllerBase(IPublishedMediaCache publishedMediaCache, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; + _publishedMediaCache = publishedMediaCache; _apiMediaWithCropsResponseBuilder = apiMediaWithCropsResponseBuilder; } - protected IPublishedMediaCache PublishedMediaCache => _publishedMediaCache - ??= _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Media - ?? throw new InvalidOperationException("Could not obtain the published media cache"); + protected IPublishedMediaCache PublishedMediaCache => _publishedMediaCache; protected IApiMediaWithCropsResponse BuildApiMediaWithCrops(IPublishedContent media) => _apiMediaWithCropsResponseBuilder.Build(media); diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/QueryMediaApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/QueryMediaApiController.cs index de872e5d86..c61aaf8764 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/QueryMediaApiController.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/QueryMediaApiController.cs @@ -21,10 +21,10 @@ public class QueryMediaApiController : MediaApiControllerBase private readonly IApiMediaQueryService _apiMediaQueryService; public QueryMediaApiController( - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedMediaCache publishedMediaCache, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder, IApiMediaQueryService apiMediaQueryService) - : base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder) + : base(publishedMediaCache, apiMediaWithCropsResponseBuilder) => _apiMediaQueryService = apiMediaQueryService; [HttpGet] diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs b/src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs index d4d63acf2b..1576d0037f 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs @@ -7,14 +7,15 @@ namespace Umbraco.Cms.Api.Delivery.Querying; public abstract class QueryOptionBase { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IPublishedContentCache _publishedContentCache; private readonly IRequestRoutingService _requestRoutingService; + public QueryOptionBase( - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; + _publishedContentCache = publishedContentCache; _requestRoutingService = requestRoutingService; } @@ -30,11 +31,9 @@ public abstract class QueryOptionBase return id; } - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - // Check if the passed value is a path of a content item var contentRoute = _requestRoutingService.GetContentRoute(queryStringValue); - IPublishedContent? contentItem = publishedSnapshot.Content?.GetByRoute(contentRoute); + IPublishedContent? contentItem = _publishedContentCache.GetByRoute(contentRoute); return contentItem?.Key; } diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/AncestorsSelector.cs b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/AncestorsSelector.cs index 5e8e7e2019..dfb91cabb2 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/AncestorsSelector.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/AncestorsSelector.cs @@ -1,19 +1,35 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Delivery.Indexing.Selectors; using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Api.Delivery.Querying.Selectors; public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler { + private readonly IPublishedContentCache _publishedContentCache; + private readonly IDocumentNavigationQueryService _navigationQueryService; private const string AncestorsSpecifier = "ancestors:"; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService) - : base(publishedSnapshotAccessor, requestRoutingService) => - _publishedSnapshotAccessor = publishedSnapshotAccessor; + public AncestorsSelector( + IPublishedContentCache publishedContentCache, + IRequestRoutingService requestRoutingService, + IDocumentNavigationQueryService navigationQueryService) + : base(publishedContentCache, requestRoutingService) + { + _publishedContentCache = publishedContentCache; + _navigationQueryService = navigationQueryService; + } + + [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + public AncestorsSelector(IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService) + : this(publishedContentCache, requestRoutingService, StaticServiceProvider.Instance.GetRequiredService()) + { + } /// public bool CanHandle(string query) @@ -37,12 +53,10 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler }; } - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - - IPublishedContent contentItem = publishedSnapshot.Content?.GetById((Guid)id) + IPublishedContent contentItem = _publishedContentCache.GetById((Guid)id) ?? throw new InvalidOperationException("Could not obtain the content cache"); - var ancestorKeys = contentItem.Ancestors().Select(a => a.Key.ToString("D")).ToArray(); + var ancestorKeys = contentItem.Ancestors(_publishedContentCache, _navigationQueryService).Select(a => a.Key.ToString("D")).ToArray(); return new SelectorOption { diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/ChildrenSelector.cs b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/ChildrenSelector.cs index 838b5da776..9392ce8e02 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/ChildrenSelector.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/ChildrenSelector.cs @@ -9,8 +9,8 @@ public sealed class ChildrenSelector : QueryOptionBase, ISelectorHandler { private const string ChildrenSpecifier = "children:"; - public ChildrenSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService) - : base(publishedSnapshotAccessor, requestRoutingService) + public ChildrenSelector(IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService) + : base(publishedContentCache, requestRoutingService) { } diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/DescendantsSelector.cs b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/DescendantsSelector.cs index e3c9bf33fd..2a7512746e 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/DescendantsSelector.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/DescendantsSelector.cs @@ -9,8 +9,8 @@ public sealed class DescendantsSelector : QueryOptionBase, ISelectorHandler { private const string DescendantsSpecifier = "descendants:"; - public DescendantsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService) - : base(publishedSnapshotAccessor, requestRoutingService) + public DescendantsSelector(IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService) + : base(publishedContentCache, requestRoutingService) { } diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs index 8a078d7f0d..7979895ba3 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Extensions; @@ -12,13 +13,15 @@ namespace Umbraco.Cms.Api.Delivery.Services; /// internal sealed class ApiMediaQueryService : IApiMediaQueryService { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IPublishedMediaCache _publishedMediaCache; private readonly ILogger _logger; + private readonly IMediaNavigationQueryService _mediaNavigationQueryService; - public ApiMediaQueryService(IPublishedSnapshotAccessor publishedSnapshotAccessor, ILogger logger) + public ApiMediaQueryService(IPublishedMediaCache publishedMediaCache, ILogger logger, IMediaNavigationQueryService mediaNavigationQueryService) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; + _publishedMediaCache = publishedMediaCache; _logger = logger; + _mediaNavigationQueryService = mediaNavigationQueryService; } /// @@ -52,8 +55,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService => TryGetByPath(path, GetRequiredPublishedMediaCache()); private IPublishedMediaCache GetRequiredPublishedMediaCache() - => _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Media - ?? throw new InvalidOperationException("Could not obtain the published media cache"); + => _publishedMediaCache; private IPublishedContent? TryGetByPath(string path, IPublishedMediaCache mediaCache) { @@ -69,7 +71,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService break; } - currentChildren = resolvedMedia.Children; + currentChildren = resolvedMedia.Children(null, _publishedMediaCache, _mediaNavigationQueryService); } return resolvedMedia; @@ -102,7 +104,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService ? mediaCache.GetById(parentKey) : TryGetByPath(childrenOf, mediaCache); - return parent?.Children ?? Array.Empty(); + return parent?.Children(null, _publishedMediaCache, _mediaNavigationQueryService) ?? Array.Empty(); } private IEnumerable? ApplyFilters(IEnumerable source, IEnumerable filters) diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs index 4b9efc03a5..2f74d86364 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs @@ -21,7 +21,7 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir private readonly GlobalSettings _globalSettings; public RequestRedirectService( - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IDomainCache domainCache, IHttpContextAccessor httpContextAccessor, IRequestStartItemProviderAccessor requestStartItemProviderAccessor, IRequestCultureService requestCultureService, @@ -29,7 +29,7 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir IApiPublishedContentCache apiPublishedContentCache, IApiContentRouteBuilder apiContentRouteBuilder, IOptions globalSettings) - : base(publishedSnapshotAccessor, httpContextAccessor, requestStartItemProviderAccessor) + : base(domainCache, httpContextAccessor, requestStartItemProviderAccessor) { _requestCultureService = requestCultureService; _redirectUrlService = redirectUrlService; diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestRoutingService.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestRoutingService.cs index 6bf0dbc887..67cf9c9fc0 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestRoutingService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestRoutingService.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; @@ -12,11 +13,11 @@ internal sealed class RequestRoutingService : RoutingServiceBase, IRequestRoutin private readonly IRequestCultureService _requestCultureService; public RequestRoutingService( - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IDomainCache domainCache, IHttpContextAccessor httpContextAccessor, IRequestStartItemProviderAccessor requestStartItemProviderAccessor, IRequestCultureService requestCultureService) - : base(publishedSnapshotAccessor, httpContextAccessor, requestStartItemProviderAccessor) => + : base(domainCache, httpContextAccessor, requestStartItemProviderAccessor) => _requestCultureService = requestCultureService; /// diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs index dd72d930bd..e79a2cbdd7 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs @@ -3,29 +3,34 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Api.Delivery.Services; internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestStartItemProvider { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IVariationContextAccessor _variationContextAccessor; private readonly IRequestPreviewService _requestPreviewService; + private readonly IDocumentNavigationQueryService _documentNavigationQueryService; + private readonly IPublishedContentCache _publishedContentCache; // this provider lifetime is Scope, so we can cache this as a field private IPublishedContent? _requestedStartContent; public RequestStartItemProvider( IHttpContextAccessor httpContextAccessor, - IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, - IRequestPreviewService requestPreviewService) + IRequestPreviewService requestPreviewService, + IDocumentNavigationQueryService documentNavigationQueryService, + IPublishedContentCache publishedContentCache) : base(httpContextAccessor) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; + _variationContextAccessor = variationContextAccessor; _requestPreviewService = requestPreviewService; + _documentNavigationQueryService = documentNavigationQueryService; + _publishedContentCache = publishedContentCache; } /// @@ -42,13 +47,11 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS return null; } - if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) == false || - publishedSnapshot?.Content == null) - { - return null; - } - - IEnumerable rootContent = publishedSnapshot.Content.GetAtRoot(_requestPreviewService.IsPreview()); + _documentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); + IEnumerable rootContent = rootKeys + .Select(_publishedContentCache.GetById) + .WhereNotNull() + .Where(x => x.IsPublished() != _requestPreviewService.IsPreview()); _requestedStartContent = Guid.TryParse(headerValue, out Guid key) ? rootContent.FirstOrDefault(c => c.Key == key) diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RoutingServiceBase.cs b/src/Umbraco.Cms.Api.Delivery/Services/RoutingServiceBase.cs index 32a8affd61..4a05521b22 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RoutingServiceBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RoutingServiceBase.cs @@ -9,16 +9,16 @@ namespace Umbraco.Cms.Api.Delivery.Services; internal abstract class RoutingServiceBase { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IDomainCache _domainCache; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IRequestStartItemProviderAccessor _requestStartItemProviderAccessor; protected RoutingServiceBase( - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IDomainCache domainCache, IHttpContextAccessor httpContextAccessor, IRequestStartItemProviderAccessor requestStartItemProviderAccessor) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; + _domainCache = domainCache; _httpContextAccessor = httpContextAccessor; _requestStartItemProviderAccessor = requestStartItemProviderAccessor; } @@ -40,15 +40,9 @@ internal abstract class RoutingServiceBase protected DomainAndUri? GetDomainAndUriForRoute(Uri contentUrl) { - IDomainCache? domainCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Domains; - if (domainCache == null) - { - throw new InvalidOperationException("Could not obtain the domain cache in the current context"); - } + IEnumerable domains = _domainCache.GetAll(false); - IEnumerable domains = domainCache.GetAll(false); - - return DomainUtilities.SelectDomain(domains, contentUrl, defaultCulture: domainCache.DefaultCulture); + return DomainUtilities.SelectDomain(domains, contentUrl, defaultCulture: _domainCache.DefaultCulture); } protected IPublishedContent? GetStartItem() diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/CollectPublishedCacheController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/CollectPublishedCacheController.cs index 1337ece12a..4cbac68446 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/CollectPublishedCacheController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/CollectPublishedCacheController.cs @@ -5,21 +5,15 @@ using Umbraco.Cms.Core.PublishedCache; namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache; +[Obsolete("This controller no longer serves a purpose")] [ApiVersion("1.0")] public class CollectPublishedCacheController : PublishedCacheControllerBase { - private readonly IPublishedSnapshotService _publishedSnapshotService; - - public CollectPublishedCacheController(IPublishedSnapshotService publishedSnapshotService) - => _publishedSnapshotService = publishedSnapshotService; - [HttpPost("collect")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task Collect(CancellationToken cancellationToken) { - GC.Collect(); - await _publishedSnapshotService.CollectAsync(); return Ok(); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs index b0e423e7e6..d48ad9fdbb 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs @@ -8,17 +8,16 @@ namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache; [ApiVersion("1.0")] public class RebuildPublishedCacheController : PublishedCacheControllerBase { - private readonly IPublishedSnapshotService _publishedSnapshotService; + private readonly IDatabaseCacheRebuilder _databaseCacheRebuilder; - public RebuildPublishedCacheController(IPublishedSnapshotService publishedSnapshotService) - => _publishedSnapshotService = publishedSnapshotService; + public RebuildPublishedCacheController(IDatabaseCacheRebuilder databaseCacheRebuilder) => _databaseCacheRebuilder = databaseCacheRebuilder; [HttpPost("rebuild")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task Rebuild(CancellationToken cancellationToken) { - _publishedSnapshotService.Rebuild(); + _databaseCacheRebuilder.Rebuild(); return await Task.FromResult(Ok()); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/StatusPublishedCacheController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/StatusPublishedCacheController.cs index e384742cb1..aad76e7dbf 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/StatusPublishedCacheController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/StatusPublishedCacheController.cs @@ -6,16 +6,12 @@ using Umbraco.Cms.Core.PublishedCache; namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache; [ApiVersion("1.0")] +[Obsolete("This no longer relevant since snapshots are no longer used")] public class StatusPublishedCacheController : PublishedCacheControllerBase { - private readonly IPublishedSnapshotStatus _publishedSnapshotStatus; - - public StatusPublishedCacheController(IPublishedSnapshotStatus publishedSnapshotStatus) - => _publishedSnapshotStatus = publishedSnapshotStatus; - [HttpGet("status")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] public async Task> Status(CancellationToken cancellationToken) - => await Task.FromResult(Ok(_publishedSnapshotStatus.GetStatus())); + => await Task.FromResult(Ok("Obsoleted")); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs index d74d299777..316dbdf51d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Linq.Expressions; +using System.Runtime.Versioning; using System.Text; using Asp.Versioning; using Microsoft.AspNetCore.Http; @@ -8,7 +9,9 @@ using Umbraco.Cms.Api.Management.ViewModels.Template.Query; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Models.TemplateQuery; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Template.Query; @@ -20,6 +23,8 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase private readonly IVariationContextAccessor _variationContextAccessor; private readonly IPublishedValueFallback _publishedValueFallback; private readonly IContentTypeService _contentTypeService; + private readonly IPublishedContentCache _contentCache; + private readonly IDocumentNavigationQueryService _documentNavigationQueryService; private static readonly string _indent = $"{Environment.NewLine} "; @@ -27,12 +32,16 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase IPublishedContentQuery publishedContentQuery, IVariationContextAccessor variationContextAccessor, IPublishedValueFallback publishedValueFallback, - IContentTypeService contentTypeService) + IContentTypeService contentTypeService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService documentNavigationQueryService) { _publishedContentQuery = publishedContentQuery; _variationContextAccessor = variationContextAccessor; _publishedValueFallback = publishedValueFallback; _contentTypeService = contentTypeService; + _contentCache = contentCache; + _documentNavigationQueryService = documentNavigationQueryService; } [HttpPost("execute")] @@ -109,13 +118,13 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase queryExpression.Append($".ChildrenOfType(\"{model.DocumentTypeAlias}\")"); return rootContent == null ? Enumerable.Empty() - : rootContent.ChildrenOfType(_variationContextAccessor, model.DocumentTypeAlias); + : rootContent.ChildrenOfType(_variationContextAccessor, _contentCache, _documentNavigationQueryService, model.DocumentTypeAlias); } queryExpression.Append(".Children()"); return rootContent == null ? Enumerable.Empty() - : rootContent.Children(_variationContextAccessor); + : rootContent.Children(_variationContextAccessor, _contentCache, _documentNavigationQueryService); } private IEnumerable ApplyFiltering(IEnumerable? filters, IEnumerable contentQuery, StringBuilder queryExpression) diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs index f77afa7347..58024d471f 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs @@ -34,7 +34,6 @@ public static partial class UmbracoBuilderExtensions .AddMvcAndRazor(configureMvc) .AddWebServer() .AddRecurringBackgroundJobs() - .AddNuCache() .AddUmbracoHybridCache() .AddDistributedCache() .AddCoreNotifications() diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs index 459eed6e69..6d0e40e2b1 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs @@ -1,12 +1,13 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Api.Management.ViewModels.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -16,10 +17,9 @@ public class DocumentUrlFactory : IDocumentUrlFactory { private readonly IDocumentUrlService _documentUrlService; - public DocumentUrlFactory( - IDocumentUrlService documentUrlService) - { + public DocumentUrlFactory(IDocumentUrlService documentUrlService) + { _documentUrlService = documentUrlService; } diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 4ba8edf445..7f8484fca4 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -19,4 +19,7 @@ public static class CacheKeys public const string ContentRecycleBinCacheKey = "recycleBin_content"; public const string MediaRecycleBinCacheKey = "recycleBin_media"; + + public const string PreviewPropertyCacheKeyPrefix = "Cache.Property.CacheValues[D:"; + public const string PropertyCacheKeyPrefix = "Cache.Property.CacheValues[P:"; } diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs index d2f3e0a6cd..8a3147f022 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs @@ -17,56 +17,30 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase { private readonly IDomainService _domainService; + private readonly IDomainCacheService _domainCacheService; private readonly IDocumentUrlService _documentUrlService; private readonly IDocumentNavigationQueryService _documentNavigationQueryService; private readonly IDocumentNavigationManagementService _documentNavigationManagementService; private readonly IContentService _contentService; private readonly IIdKeyMap _idKeyMap; - private readonly IPublishedSnapshotService _publishedSnapshotService; - - [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16")] - public ContentCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, - IIdKeyMap idKeyMap, - IDomainService domainService, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : this( - appCaches, - serializer, - publishedSnapshotService, - idKeyMap, - domainService, - eventAggregator, - factory, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService() - ) - { - - } public ContentCacheRefresher( AppCaches appCaches, IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, IIdKeyMap idKeyMap, IDomainService domainService, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory, IDocumentUrlService documentUrlService, + IDomainCacheService domainCacheService, IDocumentNavigationQueryService documentNavigationQueryService, IDocumentNavigationManagementService documentNavigationManagementService, IContentService contentService) : base(appCaches, serializer, eventAggregator, factory) { - _publishedSnapshotService = publishedSnapshotService; _idKeyMap = idKeyMap; _domainService = domainService; + _domainCacheService = domainCacheService; _documentUrlService = documentUrlService; _documentNavigationQueryService = documentNavigationQueryService; _documentNavigationManagementService = documentNavigationManagementService; @@ -159,25 +133,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray()); } } - // note: must do what's above FIRST else the repositories still have the old cached - // content and when the PublishedCachesService is notified of changes it does not see - // the new content... - - // TODO: what about this? - // should rename it, and then, this is only for Deploy, and then, ??? - // if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) - // ... - if (payloads.Any(x => x.Blueprint is false)) - { - // Only notify if the payload contains actual (non-blueprint) contents - NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, payloads); - } - base.Refresh(payloads); } @@ -326,24 +286,6 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase - /// Refreshes the publish snapshot service and if there are published changes ensures that partial view caches are - /// refreshed too - ///
- /// - /// - /// - internal static void NotifyPublishedSnapshotService(IPublishedSnapshotService service, AppCaches appCaches, JsonPayload[] payloads) - { - service.Notify(payloads, out _, out var publishedChanged); - - if (payloads.Any(x => x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) || publishedChanged) - { - // when a public version changes - appCaches.ClearPartialViewCache(); - } - } - // TODO (V14): Change into a record public class JsonPayload { diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs index e1a82d6108..dba66ec1b0 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs @@ -3,7 +3,6 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; @@ -14,25 +13,22 @@ namespace Umbraco.Cms.Core.Cache; public sealed class ContentTypeCacheRefresher : PayloadCacheRefresherBase { private readonly IContentTypeCommonRepository _contentTypeCommonRepository; - private readonly IIdKeyMap _idKeyMap; private readonly IPublishedModelFactory _publishedModelFactory; - private readonly IPublishedSnapshotService _publishedSnapshotService; + private readonly IIdKeyMap _idKeyMap; public ContentTypeCacheRefresher( AppCaches appCaches, IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, - IPublishedModelFactory publishedModelFactory, IIdKeyMap idKeyMap, IContentTypeCommonRepository contentTypeCommonRepository, IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) + ICacheRefresherNotificationFactory factory, + IPublishedModelFactory publishedModelFactory) : base(appCaches, serializer, eventAggregator, factory) { - _publishedSnapshotService = publishedSnapshotService; - _publishedModelFactory = publishedModelFactory; _idKeyMap = idKeyMap; _contentTypeCommonRepository = contentTypeCommonRepository; + _publishedModelFactory = publishedModelFactory; } #region Json @@ -115,9 +111,8 @@ public sealed class ContentTypeCacheRefresher : PayloadCacheRefresherBase - _publishedSnapshotService.Notify(payloads)); + // TODO: We need to clear the HybridCache of any content using the ContentType, but NOT the database cache here, and this should be done within the "WithSafeLiveFactoryReset" to ensure that the factory is locked in the meantime. + _publishedModelFactory.WithSafeLiveFactoryReset(() => { }); // now we can trigger the event base.Refresh(payloads); diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs index 394630fa64..f28dd89ea5 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs @@ -3,8 +3,6 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; @@ -15,21 +13,18 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase - _publishedSnapshotService.Notify(payloads)); + // TODO: We need to clear the HybridCache of any content using the ContentType, but NOT the database cache here, and this should be done within the "WithSafeLiveFactoryReset" to ensure that the factory is locked in the meantime. + _publishedModelFactory.WithSafeLiveFactoryReset(() => { }); base.Refresh(payloads); } diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs index fda11d6a91..4c765cda71 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs @@ -9,19 +9,16 @@ namespace Umbraco.Cms.Core.Cache; public sealed class DomainCacheRefresher : PayloadCacheRefresherBase { - private readonly IPublishedSnapshotService _publishedSnapshotService; private readonly IDomainCacheService _domainCacheService; public DomainCacheRefresher( AppCaches appCaches, IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory, IDomainCacheService domainCacheService) : base(appCaches, serializer, eventAggregator, factory) { - _publishedSnapshotService = publishedSnapshotService; _domainCacheService = domainCacheService; } @@ -63,10 +60,7 @@ public sealed class DomainCacheRefresher : PayloadCacheRefresherBase { + private readonly IDomainCacheService _domainCacheService; + public LanguageCacheRefresher( AppCaches appCaches, IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, IEventAggregator eventAggregator, + IDomainCacheService domainCache, ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) => - _publishedSnapshotService = publishedSnapshotService; + : base(appCaches, serializer, eventAggregator, factory) + { + _domainCacheService = domainCache; + } /// /// Clears all domain caches @@ -34,7 +38,7 @@ public sealed class LanguageCacheRefresher : PayloadCacheRefresherBase UniqueId; @@ -141,8 +144,6 @@ public sealed class LanguageCacheRefresher : PayloadCacheRefresherBase - /// Sets the published snapshot service. - /// - /// The builder. - /// A function creating a published snapshot service. - public static IUmbracoBuilder SetPublishedSnapshotService( - this IUmbracoBuilder builder, - Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } - - /// - /// Sets the published snapshot service. - /// - /// The type of the published snapshot service. - /// The builder. - public static IUmbracoBuilder SetPublishedSnapshotService(this IUmbracoBuilder builder) - where T : class, IPublishedSnapshotService - { - builder.Services.AddUnique(); - return builder; - } - - /// - /// Sets the published snapshot service. - /// - /// The builder. - /// A published snapshot service. - public static IUmbracoBuilder SetPublishedSnapshotService( - this IUmbracoBuilder builder, - IPublishedSnapshotService service) - { - builder.Services.AddUnique(service); - return builder; - } -} diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs index d33406a95a..45bb26cb0c 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs @@ -1,11 +1,9 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Core.DeliveryApi; @@ -15,47 +13,25 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder private readonly IApiContentPathProvider _apiContentPathProvider; private readonly GlobalSettings _globalSettings; private readonly IVariationContextAccessor _variationContextAccessor; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IRequestPreviewService _requestPreviewService; + private readonly IPublishedContentCache _contentCache; + private readonly IDocumentNavigationQueryService _navigationQueryService; private RequestHandlerSettings _requestSettings; - [Obsolete($"Use the constructor that does not accept {nameof(IPublishedUrlProvider)}. Will be removed in V15.")] - public ApiContentRouteBuilder( - IPublishedUrlProvider publishedUrlProvider, - IOptions globalSettings, - IVariationContextAccessor variationContextAccessor, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IRequestPreviewService requestPreviewService, - IOptionsMonitor requestSettings) - : this(StaticServiceProvider.Instance.GetRequiredService(), globalSettings, variationContextAccessor, publishedSnapshotAccessor, requestPreviewService, requestSettings) - { - } - - [Obsolete($"Use the constructor that does not accept {nameof(IPublishedUrlProvider)}. Will be removed in V15.")] - public ApiContentRouteBuilder( - IPublishedUrlProvider publishedUrlProvider, - IApiContentPathProvider apiContentPathProvider, - IOptions globalSettings, - IVariationContextAccessor variationContextAccessor, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IRequestPreviewService requestPreviewService, - IOptionsMonitor requestSettings) - : this(apiContentPathProvider, globalSettings, variationContextAccessor, publishedSnapshotAccessor, requestPreviewService, requestSettings) - { - } - public ApiContentRouteBuilder( IApiContentPathProvider apiContentPathProvider, IOptions globalSettings, IVariationContextAccessor variationContextAccessor, - IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestPreviewService requestPreviewService, - IOptionsMonitor requestSettings) + IOptionsMonitor requestSettings, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) { _apiContentPathProvider = apiContentPathProvider; _variationContextAccessor = variationContextAccessor; - _publishedSnapshotAccessor = publishedSnapshotAccessor; _requestPreviewService = requestPreviewService; + _contentCache = contentCache; + _navigationQueryService = navigationQueryService; _globalSettings = globalSettings.Value; _requestSettings = requestSettings.CurrentValue; requestSettings.OnChange(settings => _requestSettings = settings); @@ -106,7 +82,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder // we can perform fallback to the content route. if (IsInvalidContentPath(contentPath)) { - contentPath = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Content?.GetRouteById(content.Id, culture) ?? contentPath; + contentPath = _contentCache.GetRouteById(content.Id, culture) ?? contentPath; } // if the content path has still not been resolved as a valid path, the content is un-routable in this culture @@ -129,16 +105,15 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder { if (isPreview is false) { - return content.Root(); + return content.Root(_contentCache, _navigationQueryService); } + _navigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); + IEnumerable rootContent = rootKeys.Select(x => _contentCache.GetById(true, x)).WhereNotNull(); + // in very edge case scenarios during preview, content.Root() does not map to the root. // we'll code our way around it for the time being. - return _publishedSnapshotAccessor - .GetRequiredPublishedSnapshot() - .Content? - .GetAtRoot(true) - .FirstOrDefault(root => root.IsAncestorOrSelf(content)) - ?? content.Root(); + return rootContent.FirstOrDefault(root => root.IsAncestorOrSelf(content)) + ?? content.Root(_contentCache, _navigationQueryService); } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 356150536d..829dbd7ae8 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -264,9 +264,6 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddSingleton(); - // register a basic/noop published snapshot service to be replaced - Services.AddSingleton(); - // Register ValueEditorCache used for validation Services.AddSingleton(); diff --git a/src/Umbraco.Core/EmbeddedResources/Snippets/ListDescendantsFromCurrentPage.cshtml b/src/Umbraco.Core/EmbeddedResources/Snippets/ListDescendantsFromCurrentPage.cshtml index 455236ac03..f174b38495 100644 --- a/src/Umbraco.Core/EmbeddedResources/Snippets/ListDescendantsFromCurrentPage.cshtml +++ b/src/Umbraco.Core/EmbeddedResources/Snippets/ListDescendantsFromCurrentPage.cshtml @@ -1,16 +1,21 @@ @using Umbraco.Cms.Core @using Umbraco.Cms.Core.Models.PublishedContent +@using Umbraco.Cms.Core.PublishedCache @using Umbraco.Cms.Core.Routing +@using Umbraco.Cms.Core.Services.Navigation @using Umbraco.Extensions @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @inject IPublishedValueFallback PublishedValueFallback @inject IPublishedUrlProvider PublishedUrlProvider +@inject IVariationContextAccessor VariationContextAccessor +@inject IPublishedContentCache PublishedContentCache +@inject IDocumentNavigationQueryService DocumentNavigationQueryService @* This snippet creates links for every single page (no matter how deep) below the page currently being viewed by the website visitor, displayed as nested unordered HTML lists. *@ -@{ var selection = Model?.Content.Children.Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); } +@{ var selection = Model?.Content.Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService).Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); } @* Ensure that the Current Page has children *@ @if (selection?.Length > 0) @@ -28,7 +33,11 @@ @* if this child page has any children, where the property umbracoNaviHide is not True *@ @{ - var children = item.Children.Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); + var children = item + .Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService) + .Where(x => x.IsVisible(PublishedValueFallback)) + .ToArray(); + if (children.Length > 0) { @* Call a local method to display the children *@ @@ -58,7 +67,11 @@ @* if the page has any children, where the property umbracoNaviHide is not True *@ @{ - var children = item.Children.Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); + var children = item + .Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService) + .Where(x => x.IsVisible(PublishedValueFallback)) + .ToArray(); + if (children.Length > 0) { @* Recurse and call the ChildPages method to display the children *@ diff --git a/src/Umbraco.Core/EmbeddedResources/Snippets/SiteMap.cshtml b/src/Umbraco.Core/EmbeddedResources/Snippets/SiteMap.cshtml index 3a257cc161..20b31b6dcb 100644 --- a/src/Umbraco.Core/EmbeddedResources/Snippets/SiteMap.cshtml +++ b/src/Umbraco.Core/EmbeddedResources/Snippets/SiteMap.cshtml @@ -1,10 +1,15 @@ @using Umbraco.Cms.Core @using Umbraco.Cms.Core.Models.PublishedContent +@using Umbraco.Cms.Core.PublishedCache @using Umbraco.Cms.Core.Routing +@using Umbraco.Cms.Core.Services.Navigation @using Umbraco.Extensions @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @inject IPublishedValueFallback PublishedValueFallback @inject IPublishedUrlProvider PublishedUrlProvider +@inject IVariationContextAccessor VariationContextAccessor +@inject IPublishedContentCache PublishedContentCache +@inject IDocumentNavigationQueryService DocumentNavigationQueryService @* This snippet makes a list of links of all visible pages of the site, as nested unordered HTML lists. @@ -27,7 +32,10 @@ const int maxLevelForSitemap = 4; @* Select visible children *@ - var selection = node?.Children.Where(x => x.IsVisible(PublishedValueFallback) && x.Level <= maxLevelForSitemap).ToArray(); + var selection = node? + .Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService) + .Where(x => x.IsVisible(PublishedValueFallback) && x.Level <= maxLevelForSitemap) + .ToArray(); @* If any items are returned, render a list *@ if (selection?.Length > 0) diff --git a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs index ff4fd499f9..5139e23af9 100644 --- a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; namespace Umbraco.Extensions; @@ -119,16 +120,36 @@ public static class PublishedContentExtensions ///
/// The content type. /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The parent of content, of the given content type, else null. - public static T? Parent(this IPublishedContent content) + public static T? Parent( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService) where T : class, IPublishedContent { - if (content == null) + ArgumentNullException.ThrowIfNull(content); + + return content.GetParent(publishedCache, navigationQueryService) as T; + } + + private static IPublishedContent? GetParent( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService) + { + IPublishedContent? parent; + if (navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey)) { - throw new ArgumentNullException(nameof(content)); + parent = parentKey.HasValue ? publishedCache.GetById(parentKey.Value) : null; + } + else + { + throw new KeyNotFoundException($"Content with key '{content.Key}' was not found in the in-memory navigation structure."); } - return content.Parent as T; + return parent; } #endregion @@ -497,41 +518,63 @@ public static class PublishedContentExtensions /// Gets the ancestors of the content. ///
/// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The ancestors of the content, in down-top order. /// Does not consider the content itself. - public static IEnumerable Ancestors(this IPublishedContent content) => - content.AncestorsOrSelf(false, null); + public static IEnumerable Ancestors( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService) => + content.AncestorsOrSelf(publishedCache, navigationQueryService, false, null); /// /// Gets the ancestors of the content, at a level lesser or equal to a specified level. /// /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The level. /// The ancestors of the content, at a level lesser or equal to the specified level, in down-top order. /// Does not consider the content itself. Only content that are "high enough" in the tree are returned. - public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) => - content.AncestorsOrSelf(false, n => n.Level <= maxLevel); + public static IEnumerable Ancestors( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int maxLevel) => + content.AncestorsOrSelf(publishedCache, navigationQueryService, false, n => n.Level <= maxLevel); /// /// Gets the ancestors of the content, of a specified content type. /// /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The content type. /// The ancestors of the content, of the specified content type, in down-top order. /// Does not consider the content itself. Returns all ancestors, of the specified content type. - public static IEnumerable Ancestors(this IPublishedContent content, string contentTypeAlias) => - content.AncestorsOrSelf(false, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); + public static IEnumerable Ancestors( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string contentTypeAlias) => + content.AncestorsOrSelf(publishedCache, navigationQueryService, false, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); /// /// Gets the ancestors of the content, of a specified content type. /// /// The content type. /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The ancestors of the content, of the specified content type, in down-top order. /// Does not consider the content itself. Returns all ancestors, of the specified content type. - public static IEnumerable Ancestors(this IPublishedContent content) + public static IEnumerable Ancestors( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService) where T : class, IPublishedContent => - content.Ancestors().OfType(); + content.Ancestors(publishedCache, navigationQueryService).OfType(); /// /// Gets the ancestors of the content, at a level lesser or equal to a specified level, and of a specified content @@ -539,6 +582,8 @@ public static class PublishedContentExtensions /// /// The content type. /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The level. /// /// The ancestors of the content, at a level lesser or equal to the specified level, and of the specified @@ -548,22 +593,33 @@ public static class PublishedContentExtensions /// Does not consider the content itself. Only content that are "high enough" in the trees, and of the /// specified content type, are returned. /// - public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) + public static IEnumerable Ancestors( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int maxLevel) where T : class, IPublishedContent => - content.Ancestors(maxLevel).OfType(); + content.Ancestors(publishedCache, navigationQueryService, maxLevel).OfType(); /// /// Gets the content and its ancestors. /// /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The content and its ancestors, in down-top order. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content) => - content.AncestorsOrSelf(true, null); + public static IEnumerable AncestorsOrSelf( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService) => + content.AncestorsOrSelf(publishedCache, navigationQueryService, true, null); /// /// Gets the content and its ancestors, at a level lesser or equal to a specified level. /// /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The level. /// /// The content and its ancestors, at a level lesser or equal to the specified level, @@ -573,30 +629,44 @@ public static class PublishedContentExtensions /// Only content that are "high enough" in the tree are returned. So it may or may not begin /// with the content itself, depending on its level. /// - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) => - content.AncestorsOrSelf(true, n => n.Level <= maxLevel); + public static IEnumerable AncestorsOrSelf( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int maxLevel) => + content.AncestorsOrSelf(publishedCache, navigationQueryService, true, n => n.Level <= maxLevel); /// /// Gets the content and its ancestors, of a specified content type. /// /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The content type. /// The content and its ancestors, of the specified content type, in down-top order. /// May or may not begin with the content itself, depending on its content type. - public static IEnumerable - AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias) => - content.AncestorsOrSelf(true, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); + public static IEnumerable AncestorsOrSelf( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string contentTypeAlias) => + content.AncestorsOrSelf(publishedCache, navigationQueryService, true, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); /// /// Gets the content and its ancestors, of a specified content type. /// /// The content type. /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The content and its ancestors, of the specified content type, in down-top order. /// May or may not begin with the content itself, depending on its content type. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content) + public static IEnumerable AncestorsOrSelf( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService) where T : class, IPublishedContent => - content.AncestorsOrSelf().OfType(); + content.AncestorsOrSelf(publishedCache, navigationQueryService).OfType(); /// /// Gets the content and its ancestor, at a lever lesser or equal to a specified level, and of a specified content @@ -604,69 +674,104 @@ public static class PublishedContentExtensions /// /// The content type. /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The level. /// /// The content and its ancestors, at a level lesser or equal to the specified level, and of the specified /// content type, in down-top order. /// /// May or may not begin with the content itself, depending on its level and content type. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) + public static IEnumerable AncestorsOrSelf( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int maxLevel) where T : class, IPublishedContent => - content.AncestorsOrSelf(maxLevel).OfType(); + content.AncestorsOrSelf(publishedCache, navigationQueryService, maxLevel).OfType(); /// /// Gets the ancestor of the content, ie its parent. /// /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The ancestor of the content. /// This method is here for consistency purposes but does not make much sense. - public static IPublishedContent? Ancestor(this IPublishedContent content) => content.Parent; + public static IPublishedContent? Ancestor( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService) + => content.GetParent(publishedCache, navigationQueryService); /// /// Gets the nearest ancestor of the content, at a lever lesser or equal to a specified level. /// /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The level. /// The nearest (in down-top order) ancestor of the content, at a level lesser or equal to the specified level. /// Does not consider the content itself. May return null. - public static IPublishedContent? Ancestor(this IPublishedContent content, int maxLevel) => - content.EnumerateAncestors(false).FirstOrDefault(x => x.Level <= maxLevel); + public static IPublishedContent? Ancestor( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int maxLevel) => + content.EnumerateAncestors(publishedCache, navigationQueryService, false).FirstOrDefault(x => x.Level <= maxLevel); /// /// Gets the nearest ancestor of the content, of a specified content type. /// /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The content type alias. /// The nearest (in down-top order) ancestor of the content, of the specified content type. /// Does not consider the content itself. May return null. - public static IPublishedContent? Ancestor(this IPublishedContent content, string contentTypeAlias) => content - .EnumerateAncestors(false).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + public static IPublishedContent? Ancestor( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string contentTypeAlias) => + content.EnumerateAncestors(publishedCache, navigationQueryService, false).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); /// /// Gets the nearest ancestor of the content, of a specified content type. /// /// The content type. /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The nearest (in down-top order) ancestor of the content, of the specified content type. /// Does not consider the content itself. May return null. - public static T? Ancestor(this IPublishedContent content) + public static T? Ancestor( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService) where T : class, IPublishedContent => - content.Ancestors().FirstOrDefault(); + content.Ancestors(publishedCache, navigationQueryService).FirstOrDefault(); /// /// Gets the nearest ancestor of the content, at the specified level and of the specified content type. /// /// The content type. /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The level. /// The ancestor of the content, at the specified level and of the specified content type. /// /// Does not consider the content itself. If the ancestor at the specified level is /// not of the specified type, returns null. /// - public static T? Ancestor(this IPublishedContent content, int maxLevel) + public static T? Ancestor( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int maxLevel) where T : class, IPublishedContent => - content.Ancestors(maxLevel).FirstOrDefault(); + content.Ancestors(publishedCache, navigationQueryService, maxLevel).FirstOrDefault(); /// /// Gets the content or its nearest ancestor. @@ -680,32 +785,49 @@ public static class PublishedContentExtensions /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level. /// /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The level. /// The content or its nearest (in down-top order) ancestor, at a level lesser or equal to the specified level. /// May or may not return the content itself depending on its level. May return null. - public static IPublishedContent AncestorOrSelf(this IPublishedContent content, int maxLevel) => - content.EnumerateAncestors(true).FirstOrDefault(x => x.Level <= maxLevel) ?? content; + public static IPublishedContent AncestorOrSelf( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int maxLevel) => + content.EnumerateAncestors(publishedCache, navigationQueryService, true).FirstOrDefault(x => x.Level <= maxLevel) ?? content; /// /// Gets the content or its nearest ancestor, of a specified content type. /// /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The content type. /// The content or its nearest (in down-top order) ancestor, of the specified content type. /// May or may not return the content itself depending on its content type. May return null. - public static IPublishedContent AncestorOrSelf(this IPublishedContent content, string contentTypeAlias) => content - .EnumerateAncestors(true).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)) ?? content; + public static IPublishedContent AncestorOrSelf( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string contentTypeAlias) => content + .EnumerateAncestors(publishedCache, navigationQueryService, true).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)) ?? content; /// /// Gets the content or its nearest ancestor, of a specified content type. /// /// The content type. /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The content or its nearest (in down-top order) ancestor, of the specified content type. /// May or may not return the content itself depending on its content type. May return null. - public static T? AncestorOrSelf(this IPublishedContent content) + public static T? AncestorOrSelf( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService) where T : class, IPublishedContent => - content.AncestorsOrSelf().FirstOrDefault(); + content.AncestorsOrSelf(publishedCache, navigationQueryService).FirstOrDefault(); /// /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level, and of a specified @@ -713,15 +835,26 @@ public static class PublishedContentExtensions /// /// The content type. /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The level. /// - public static T? AncestorOrSelf(this IPublishedContent content, int maxLevel) + public static T? AncestorOrSelf( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int maxLevel) where T : class, IPublishedContent => - content.AncestorsOrSelf(maxLevel).FirstOrDefault(); + content.AncestorsOrSelf(publishedCache, navigationQueryService, maxLevel).FirstOrDefault(); - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func? func) + public static IEnumerable AncestorsOrSelf( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + bool orSelf, + Func? func) { - IEnumerable ancestorsOrSelf = content.EnumerateAncestors(orSelf); + IEnumerable ancestorsOrSelf = content.EnumerateAncestors(publishedCache, navigationQueryService, orSelf); return func == null ? ancestorsOrSelf : ancestorsOrSelf.Where(func); } @@ -729,9 +862,15 @@ public static class PublishedContentExtensions /// Enumerates ancestors of the content, bottom-up. ///
/// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// Indicates whether the content should be included. /// Enumerates bottom-up ie walking up the tree (parent, grand-parent, etc). - internal static IEnumerable EnumerateAncestors(this IPublishedContent? content, bool orSelf) + internal static IEnumerable EnumerateAncestors( + this IPublishedContent? content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + bool orSelf) { if (content == null) { @@ -743,7 +882,7 @@ public static class PublishedContentExtensions yield return content; } - while ((content = content.Parent) != null) + while ((content = content.GetParent(publishedCache, navigationQueryService)) != null) { yield return content; } @@ -757,18 +896,26 @@ public static class PublishedContentExtensions /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified . ///
/// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// Indicates whether the specified content should be included. /// /// The breadcrumbs (ancestors and self, top to bottom) for the specified . /// - public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) => - content.AncestorsOrSelf(andSelf, null).Reverse(); + public static IEnumerable Breadcrumbs( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + bool andSelf = true) => + content.AncestorsOrSelf(publishedCache, navigationQueryService, andSelf, null).Reverse(); /// /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level /// higher or equal to . /// /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// The minimum level. /// Indicates whether the specified content should be included. /// @@ -777,9 +924,11 @@ public static class PublishedContentExtensions /// public static IEnumerable Breadcrumbs( this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, int minLevel, bool andSelf = true) => - content.AncestorsOrSelf(andSelf, n => n.Level >= minLevel).Reverse(); + content.AncestorsOrSelf(publishedCache, navigationQueryService, andSelf, n => n.Level >= minLevel).Reverse(); /// /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level @@ -787,12 +936,18 @@ public static class PublishedContentExtensions /// /// The root content type. /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// Indicates whether the specified content should be included. /// /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher /// or equal to the specified root content type . /// - public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) + public static IEnumerable Breadcrumbs( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + bool andSelf = true) where T : class, IPublishedContent { static IEnumerable TakeUntil(IEnumerable source, Func predicate) @@ -807,7 +962,7 @@ public static class PublishedContentExtensions } } - return TakeUntil(content.AncestorsOrSelf(andSelf, null), n => n is T).Reverse(); + return TakeUntil(content.AncestorsOrSelf(publishedCache, navigationQueryService, andSelf, null), n => n is T).Reverse(); } #endregion @@ -819,18 +974,25 @@ public static class PublishedContentExtensions ///
/// /// Variation context accessor. + /// /// /// /// The specific culture to filter for. If null is used the current culture is used. (Default is /// null) /// + /// /// /// /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot /// public static IEnumerable DescendantsOrSelfOfType( - this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, string docTypeAlias, string? culture = null) => parentNodes.SelectMany(x => - x.DescendantsOrSelfOfType(variationContextAccessor, docTypeAlias, culture)); + this IEnumerable parentNodes, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string docTypeAlias, + string? culture = null) => parentNodes.SelectMany(x => + x.DescendantsOrSelfOfType(variationContextAccessor, publishedCache, navigationQueryService, docTypeAlias, culture)); /// /// Returns all DescendantsOrSelf of all content referenced @@ -845,9 +1007,14 @@ public static class PublishedContentExtensions /// /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot /// - public static IEnumerable DescendantsOrSelf(this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, string? culture = null) + public static IEnumerable DescendantsOrSelf( + this IEnumerable parentNodes, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) where T : class, IPublishedContent => - parentNodes.SelectMany(x => x.DescendantsOrSelf(variationContextAccessor, culture)); + parentNodes.SelectMany(x => x.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, culture)); // as per XPath 1.0 specs �2.2, // - the descendant axis contains the descendants of the context node; a descendant is a child or a child of a child and so on; thus @@ -867,85 +1034,199 @@ public static class PublishedContentExtensions // - every node occurs before all of its children and descendants. // - the relative order of siblings is the order in which they occur in the children property of their parent node. // - children and descendants occur before following siblings. - public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, false, null, culture); + public static IEnumerable Descendants( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, false, null, culture); - public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, false, p => p.Level >= level, culture); + public static IEnumerable Descendants( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int level, + string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, false, p => p.Level >= level, culture); - public static IEnumerable DescendantsOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, false, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + public static IEnumerable DescendantsOfType( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string contentTypeAlias, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, false, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); - public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + public static IEnumerable Descendants( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) where T : class, IPublishedContent => - content.Descendants(variationContextAccessor, culture).OfType(); + content.Descendants(variationContextAccessor, publishedCache, navigationQueryService, culture).OfType(); - public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) + public static IEnumerable Descendants( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int level, + string? culture = null) where T : class, IPublishedContent => - content.Descendants(variationContextAccessor, level, culture).OfType(); + content.Descendants(variationContextAccessor, publishedCache, navigationQueryService, level, culture).OfType(); - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, true, null, culture); + public static IEnumerable DescendantsOrSelf( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, true, null, culture); - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, true, p => p.Level >= level, culture); + public static IEnumerable DescendantsOrSelf( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int level, + string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, true, p => p.Level >= level, culture); - public static IEnumerable DescendantsOrSelfOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, true, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + public static IEnumerable DescendantsOrSelfOfType( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string contentTypeAlias, + string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, true, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + public static IEnumerable DescendantsOrSelf( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) where T : class, IPublishedContent => - content.DescendantsOrSelf(variationContextAccessor, culture).OfType(); + content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, culture).OfType(); - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) + public static IEnumerable DescendantsOrSelf( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int level, + string? culture = null) where T : class, IPublishedContent => - content.DescendantsOrSelf(variationContextAccessor, level, culture).OfType(); + content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, level, culture).OfType(); - public static IPublishedContent? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => - content.Children(variationContextAccessor, culture)?.FirstOrDefault(); + public static IPublishedContent? Descendant( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) => + content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture)?.FirstOrDefault(); - public static IPublishedContent? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, false, culture).FirstOrDefault(x => x.Level == level); + public static IPublishedContent? Descendant( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int level, + string? culture = null) => content + .EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, false, culture).FirstOrDefault(x => x.Level == level); - public static IPublishedContent? DescendantOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, false, culture) + public static IPublishedContent? DescendantOfType( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string contentTypeAlias, + string? culture = null) => content + .EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, false, culture) .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); - public static T? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + public static T? Descendant( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) where T : class, IPublishedContent => - content.EnumerateDescendants(variationContextAccessor, false, culture).FirstOrDefault(x => x is T) as T; + content.EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, false, culture).FirstOrDefault(x => x is T) as T; - public static T? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) + public static T? Descendant( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int level, + string? culture = null) where T : class, IPublishedContent => - content.Descendant(variationContextAccessor, level, culture) as T; + content.Descendant(variationContextAccessor, publishedCache, navigationQueryService, level, culture) as T; public static IPublishedContent DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => content; - public static IPublishedContent? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, true, culture).FirstOrDefault(x => x.Level == level); + public static IPublishedContent? DescendantOrSelf( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int level, + string? culture = null) => content + .EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, true, culture).FirstOrDefault(x => x.Level == level); - public static IPublishedContent? DescendantOrSelfOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, true, culture) + public static IPublishedContent? DescendantOrSelfOfType( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string contentTypeAlias, + string? culture = null) => content + .EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, true, culture) .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); - public static T? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + public static T? DescendantOrSelf( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) where T : class, IPublishedContent => - content.EnumerateDescendants(variationContextAccessor, true, culture).FirstOrDefault(x => x is T) as T; + content.EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, true, culture).FirstOrDefault(x => x is T) as T; - public static T? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) + public static T? DescendantOrSelf( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + int level, + string? culture = null) where T : class, IPublishedContent => - content.DescendantOrSelf(variationContextAccessor, level, culture) as T; + content.DescendantOrSelf(variationContextAccessor, publishedCache, navigationQueryService, level, culture) as T; internal static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, bool orSelf, Func? func, string? culture = null) => - content.EnumerateDescendants(variationContextAccessor, orSelf, culture) + content.EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, orSelf, culture) .Where(x => func == null || func(x)); - internal static IEnumerable EnumerateDescendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, bool orSelf, string? culture = null) + internal static IEnumerable EnumerateDescendants( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + bool orSelf, + string? culture = null) { if (content == null) { @@ -957,25 +1238,30 @@ public static class PublishedContentExtensions yield return content; } - IEnumerable? children = content.Children(variationContextAccessor, culture); + IEnumerable? children = content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture); if (children is not null) { foreach (IPublishedContent desc in children.SelectMany(x => - x.EnumerateDescendants(variationContextAccessor, culture))) + x.EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, culture))) { yield return desc; } } } - internal static IEnumerable EnumerateDescendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + internal static IEnumerable EnumerateDescendants( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) { yield return content; - IEnumerable? children = content.Children(variationContextAccessor, culture); + IEnumerable? children = content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture); if (children is not null) { foreach (IPublishedContent desc in children.SelectMany(x => - x.EnumerateDescendants(variationContextAccessor, culture))) + x.EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, culture))) { yield return desc; } @@ -991,10 +1277,12 @@ public static class PublishedContentExtensions /// /// The content item. /// + /// /// /// The specific culture to get the URL children for. Default is null which will use the current culture in /// /// + /// /// /// Gets children that are available for the specified culture. /// Children are sorted by their sortOrder. @@ -1012,18 +1300,32 @@ public static class PublishedContentExtensions /// However, if an empty string is specified only invariant children are returned. /// /// - public static IEnumerable Children(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) + public static IEnumerable Children( + this IPublishedContent content, + IVariationContextAccessor? variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) { // handle context culture for variant - if (culture == null) + if (culture is null) { culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } - IEnumerable? children = content.ChildrenForAllCultures; - return (culture == "*" - ? children : children?.Where(x => x.IsInvariantOrHasCulture(culture))) - ?? Enumerable.Empty(); + if (navigationQueryService.TryGetChildrenKeys(content.Key, out IEnumerable childrenKeys) is false) + { + return []; + } + + IEnumerable children = childrenKeys.Select(publishedCache.GetById).WhereNotNull(); + + if (culture == "*") + { + return children; + } + + return children.Where(x => x.IsInvariantOrHasCulture(culture)) ?? []; } /// @@ -1031,11 +1333,13 @@ public static class PublishedContentExtensions /// /// The content. /// The accessor for VariationContext + /// /// The predicate. /// /// The specific culture to filter for. If null is used the current culture is used. (Default is /// null) /// + /// /// The children of the content, filtered by the predicate. /// /// Children are sorted by their sortOrder. @@ -1043,23 +1347,34 @@ public static class PublishedContentExtensions public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, Func predicate, string? culture = null) => - content.Children(variationContextAccessor, culture).Where(predicate); + content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture).Where(predicate); /// /// Gets the children of the content, of any of the specified types. /// /// The content. + /// /// The accessor for the VariationContext /// /// The specific culture to filter for. If null is used the current culture is used. (Default is /// null) /// /// The content type alias. + /// /// The children of the content, of any of the specified types. - public static IEnumerable ChildrenOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? contentTypeAlias, string? culture = null) => - content.Children(variationContextAccessor, x => x.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + public static IEnumerable ChildrenOfType( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? contentTypeAlias, + string? culture = null) => + content.Children(variationContextAccessor, publishedCache, navigationQueryService, x => x.ContentType.Alias.InvariantEquals(contentTypeAlias), + culture); /// /// Gets the children of the content, of a given content type. @@ -1075,31 +1390,71 @@ public static class PublishedContentExtensions /// /// Children are sorted by their sortOrder. /// - public static IEnumerable Children(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + public static IEnumerable Children( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) where T : class, IPublishedContent => - content.Children(variationContextAccessor, culture).OfType(); + content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture).OfType(); - public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => - content.Children(variationContextAccessor, culture)?.FirstOrDefault(); + public static IPublishedContent? FirstChild( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) => + content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture)?.FirstOrDefault(); /// /// Gets the first child of the content, of a given content type. /// - public static IPublishedContent? FirstChildOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => - content.ChildrenOfType(variationContextAccessor, contentTypeAlias, culture)?.FirstOrDefault(); + public static IPublishedContent? FirstChildOfType( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string contentTypeAlias, + string? culture = null) => + content.ChildrenOfType(variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture)?.FirstOrDefault(); - public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) => content.Children(variationContextAccessor, predicate, culture)?.FirstOrDefault(); + public static IPublishedContent? FirstChild( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + Func predicate, + string? culture = null) + => content.Children(variationContextAccessor, publishedCache, navigationQueryService, predicate, culture)?.FirstOrDefault(); - public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Guid uniqueId, string? culture = null) => content - .Children(variationContextAccessor, x => x.Key == uniqueId, culture)?.FirstOrDefault(); + public static IPublishedContent? FirstChild( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + Guid uniqueId, + string? culture = null) => content + .Children(variationContextAccessor, publishedCache, navigationQueryService, x => x.Key == uniqueId, culture)?.FirstOrDefault(); - public static T? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + public static T? FirstChild( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) where T : class, IPublishedContent => - content.Children(variationContextAccessor, culture)?.FirstOrDefault(); + content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture)?.FirstOrDefault(); - public static T? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) + public static T? FirstChild( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + Func predicate, + string? culture = null) where T : class, IPublishedContent => - content.Children(variationContextAccessor, culture)?.FirstOrDefault(predicate); + content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture)?.FirstOrDefault(predicate); #endregion @@ -1109,22 +1464,24 @@ public static class PublishedContentExtensions /// Gets the siblings of the content. /// /// The content. - /// Published snapshot instance + /// The navigation service /// Variation context accessor. /// /// The specific culture to filter for. If null is used the current culture is used. (Default is /// null) /// + /// The content cache instance. /// The siblings of the content. /// /// Note that in V7 this method also return the content node self. /// public static IEnumerable Siblings( this IPublishedContent content, - IPublishedSnapshot? publishedSnapshot, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, IVariationContextAccessor variationContextAccessor, string? culture = null) => - SiblingsAndSelf(content, publishedSnapshot, variationContextAccessor, culture) + SiblingsAndSelf(content, publishedCache, navigationQueryService, variationContextAccessor, culture) ?.Where(x => x.Id != content.Id) ?? Enumerable.Empty(); /// @@ -1144,11 +1501,12 @@ public static class PublishedContentExtensions /// public static IEnumerable SiblingsOfType( this IPublishedContent content, - IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, string contentTypeAlias, string? culture = null) => - SiblingsAndSelfOfType(content, publishedSnapshot, variationContextAccessor, contentTypeAlias, culture) + SiblingsAndSelfOfType(content, variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture) ?.Where(x => x.Id != content.Id) ?? Enumerable.Empty(); /// @@ -1166,16 +1524,22 @@ public static class PublishedContentExtensions /// /// Note that in V7 this method also return the content node self. /// - public static IEnumerable Siblings(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string? culture = null) + public static IEnumerable Siblings( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) where T : class, IPublishedContent => - SiblingsAndSelf(content, publishedSnapshot, variationContextAccessor, culture) + SiblingsAndSelf(content, variationContextAccessor, publishedCache, navigationQueryService, culture) ?.Where(x => x.Id != content.Id) ?? Enumerable.Empty(); /// /// Gets the siblings of the content including the node itself to indicate the position. /// /// The content. - /// Published snapshot instance + /// Cache instance. + /// The navigation service. /// Variation context accessor. /// /// The specific culture to filter for. If null is used the current culture is used. (Default is @@ -1184,13 +1548,30 @@ public static class PublishedContentExtensions /// The siblings of the content including the node itself. public static IEnumerable? SiblingsAndSelf( this IPublishedContent content, - IPublishedSnapshot? publishedSnapshot, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, IVariationContextAccessor variationContextAccessor, - string? culture = null) => - content.Parent != null - ? content.Parent.Children(variationContextAccessor, culture) - : publishedSnapshot?.Content?.GetAtRoot(culture) + string? culture = null) + { + var success = navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey); + + if (success is false || parentKey is null) + { + if (navigationQueryService.TryGetRootKeys(out IEnumerable childrenKeys) is false) + { + return null; + } + + return childrenKeys + .Select(publishedCache.GetById) + .WhereNotNull() .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + } + + return navigationQueryService.TryGetChildrenKeys(parentKey.Value, out IEnumerable siblingKeys) is false + ? null + : siblingKeys.Select(publishedCache.GetById).WhereNotNull(); + } /// /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. @@ -1206,15 +1587,33 @@ public static class PublishedContentExtensions /// The siblings of the content including the node itself, of the given content type. public static IEnumerable SiblingsAndSelfOfType( this IPublishedContent content, - IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, string contentTypeAlias, - string? culture = null) => - (content.Parent != null - ? content.Parent.ChildrenOfType(variationContextAccessor, contentTypeAlias, culture) - : publishedSnapshot?.Content?.GetAtRoot(culture).OfTypes(contentTypeAlias) - .WhereIsInvariantOrHasCulture(variationContextAccessor, culture)) - ?? Enumerable.Empty(); + string? culture = null) + { + + var parentSuccess = navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey); + + IPublishedContent? parent = parentKey is null ? null : publishedCache.GetById(parentKey.Value); + + if (parentSuccess is false || parent is null) + { + if (navigationQueryService.TryGetRootKeys(out IEnumerable childrenKeys) is false) + { + return Enumerable.Empty(); + } + + return childrenKeys + .Select(publishedCache.GetById) + .WhereNotNull() + .OfTypes(contentTypeAlias) + .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + } + + return parent.ChildrenOfType(variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture); + } /// /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. @@ -1230,15 +1629,32 @@ public static class PublishedContentExtensions /// The siblings of the content including the node itself, of the given content type. public static IEnumerable SiblingsAndSelf( this IPublishedContent content, - IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, string? culture = null) - where T : class, IPublishedContent => - (content.Parent != null - ? content.Parent.Children(variationContextAccessor, culture) - : publishedSnapshot?.Content?.GetAtRoot(culture).OfType() - .WhereIsInvariantOrHasCulture(variationContextAccessor, culture)) - ?? Enumerable.Empty(); + where T : class, IPublishedContent + { + var parentSuccess = navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey); + IPublishedContent? parent = parentKey is null ? null : publishedCache.GetById(parentKey.Value); + + if (parentSuccess is false || parent is null) + { + var rootSuccess = navigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); + if (rootSuccess is false) + { + return []; + } + + return rootKeys + .Select(publishedCache.GetById) + .WhereNotNull() + .WhereIsInvariantOrHasCulture(variationContextAccessor, culture) + .OfType(); + } + + return parent.Children(variationContextAccessor, publishedCache, navigationQueryService, culture); + } #endregion @@ -1248,6 +1664,8 @@ public static class PublishedContentExtensions /// Gets the root content (ancestor or self at level 1) for the specified . /// /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// /// The root content (ancestor or self at level 1) for the specified . /// @@ -1256,7 +1674,10 @@ public static class PublishedContentExtensions /// with maxLevel /// set to 1. /// - public static IPublishedContent Root(this IPublishedContent content) => content.AncestorOrSelf(1); + public static IPublishedContent Root( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService) => content.AncestorOrSelf(publishedCache, navigationQueryService, 1); /// /// Gets the root content (ancestor or self at level 1) for the specified if it's of the @@ -1264,6 +1685,8 @@ public static class PublishedContentExtensions /// /// The content type. /// The content. + /// The content cache. + /// The query service for the in-memory navigation structure. /// /// The root content (ancestor or self at level 1) for the specified of content type /// . @@ -1273,9 +1696,12 @@ public static class PublishedContentExtensions /// with /// maxLevel set to 1. /// - public static T? Root(this IPublishedContent content) + public static T? Root( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService) where T : class, IPublishedContent => - content.AncestorOrSelf(1); + content.AncestorOrSelf(publishedCache, navigationQueryService, 1); #endregion @@ -1315,13 +1741,15 @@ public static class PublishedContentExtensions public static DataTable ChildrenAsTable( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, IPublishedUrlProvider publishedUrlProvider, string contentTypeAliasFilter = "", string? culture = null) - => GenerateDataTable(content, variationContextAccessor, contentTypeService, mediaTypeService, memberTypeService, publishedUrlProvider, contentTypeAliasFilter, culture); + => GenerateDataTable(content, variationContextAccessor, publishedCache, navigationQueryService, contentTypeService, mediaTypeService, memberTypeService, publishedUrlProvider, contentTypeAliasFilter, culture); /// /// Gets the children of the content in a DataTable. @@ -1341,6 +1769,8 @@ public static class PublishedContentExtensions private static DataTable GenerateDataTable( IPublishedContent content, IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, @@ -1349,10 +1779,10 @@ public static class PublishedContentExtensions string? culture = null) { IPublishedContent? firstNode = contentTypeAliasFilter.IsNullOrWhiteSpace() - ? content.Children(variationContextAccessor, culture)?.Any() ?? false - ? content.Children(variationContextAccessor, culture)?.ElementAt(0) + ? content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture)?.Any() ?? false + ? content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture)?.ElementAt(0) : null - : content.Children(variationContextAccessor, culture) + : content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture) ?.FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAliasFilter)); if (firstNode == null) { @@ -1375,7 +1805,7 @@ public static class PublishedContentExtensions List>, IEnumerable>>> tableData = DataTableExtensions.CreateTableData(); IOrderedEnumerable? children = - content.Children(variationContextAccessor)?.OrderBy(x => x.SortOrder); + content.Children(variationContextAccessor, publishedCache, navigationQueryService)?.OrderBy(x => x.SortOrder); if (children is not null) { // loop through each child and create row data for it diff --git a/src/Umbraco.Core/Extensions/PublishedSnapshotAccessorExtensions.cs b/src/Umbraco.Core/Extensions/PublishedSnapshotAccessorExtensions.cs deleted file mode 100644 index 5e6d356674..0000000000 --- a/src/Umbraco.Core/Extensions/PublishedSnapshotAccessorExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Umbraco.Cms.Core.PublishedCache; - -namespace Umbraco.Extensions; - -public static class PublishedSnapshotAccessorExtensions -{ - public static IPublishedSnapshot GetRequiredPublishedSnapshot( - this IPublishedSnapshotAccessor publishedSnapshotAccessor) - { - if (publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)) - { - return publishedSnapshot!; - } - - throw new InvalidOperationException("Wasn't possible to a get a valid Snapshot"); - } -} diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index 22bbfae7c7..233a67fa62 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -209,7 +209,7 @@ public static class ContentRepositoryExtensions { foreach (IPropertyValue pvalue in otherProperty.Values) { - if (((otherProperty?.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, true) ?? false) && + if (((otherProperty?.PropertyType?.SupportsVariation(pvalue.Culture, pvalue.Segment, true) ?? false) && (culture == "*" ||(pvalue.Culture?.InvariantEquals(culture) ?? false))) || otherProperty?.PropertyType?.Variations == ContentVariation.Nothing) { diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs index 9028a501ad..8792baaecc 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs @@ -100,6 +100,7 @@ public interface IPublishedContent : IPublishedElement /// Gets the parent of the content item. /// /// The parent of root content is null. + [Obsolete("Please use IDocumentNavigationQueryService.TryGetParentKey() instead. Scheduled for removal in V16.")] IPublishedContent? Parent { get; } /// @@ -141,10 +142,6 @@ public interface IPublishedContent : IPublishedElement /// /// Gets the children of the content item that are available for the current culture. /// + [Obsolete("Please use IDocumentNavigationQueryService.TryGetChildrenKeys() instead. Scheduled for removal in V16.")] IEnumerable Children { get; } - - /// - /// Gets all the children of the content item, regardless of whether they are available for the current culture. - /// - IEnumerable ChildrenForAllCultures { get; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs index c9394e1e27..56f7789578 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs @@ -1,4 +1,8 @@ using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models.PublishedContent @@ -33,9 +37,11 @@ namespace Umbraco.Cms.Core.Models.PublishedContent public abstract int SortOrder { get; } /// + [Obsolete("Not supported for members, scheduled for removal in v17")] public abstract int Level { get; } /// + [Obsolete("Not supported for members, scheduled for removal in v17")] public abstract string Path { get; } /// @@ -66,18 +72,41 @@ namespace Umbraco.Cms.Core.Models.PublishedContent public abstract bool IsPublished(string? culture = null); /// + [Obsolete("Please use TryGetParentKey() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")] public abstract IPublishedContent? Parent { get; } + // FIXME /// - public virtual IEnumerable Children => this.Children(_variationContextAccessor); + [Obsolete("Please use TryGetChildrenKeys() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")] + public virtual IEnumerable Children => GetChildren(); - /// - public abstract IEnumerable ChildrenForAllCultures { get; } /// public abstract IEnumerable Properties { get; } /// public abstract IPublishedProperty? GetProperty(string alias); + + private IEnumerable GetChildren() + { + INavigationQueryService? navigationQueryService; + IPublishedCache? publishedCache; + + switch (ContentType.ItemType) + { + case PublishedItemType.Content: + publishedCache = StaticServiceProvider.Instance.GetRequiredService(); + navigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); + break; + case PublishedItemType.Media: + publishedCache = StaticServiceProvider.Instance.GetRequiredService(); + navigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); + break; + default: + throw new NotImplementedException("Level is not implemented for " + ContentType.ItemType); + } + + return this.Children(_variationContextAccessor, publishedCache, navigationQueryService); + } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs index ddeff558dd..807943edff 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -1,4 +1,9 @@ using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models.PublishedContent; @@ -90,6 +95,7 @@ public abstract class PublishedContentWrapped : IPublishedContent public virtual PublishedItemType ItemType => _content.ItemType; /// + [Obsolete("Please use TryGetParentKey() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")] public virtual IPublishedContent? Parent => _content.Parent; /// @@ -99,11 +105,9 @@ public abstract class PublishedContentWrapped : IPublishedContent public virtual bool IsPublished(string? culture = null) => _content.IsPublished(culture); /// + [Obsolete("Please use TryGetChildrenKeys() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")] public virtual IEnumerable Children => _content.Children; - /// - public virtual IEnumerable ChildrenForAllCultures => _content.ChildrenForAllCultures; - /// public virtual IEnumerable Properties => _content.Properties; diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs index 8a50323f12..bec5250e01 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs @@ -1,4 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models.PublishedContent; @@ -184,7 +188,7 @@ public class PublishedValueFallback : IPublishedValueFallback IPublishedProperty? property; // if we are here, content's property has no value do { - content = content?.Parent; + content = content?.Parent(StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()); IPublishedPropertyType? propertyType = content?.ContentType.GetPropertyType(alias); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs index 014a0a1a8c..6fdf5d3246 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs @@ -12,17 +12,14 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; public class MemberPickerValueConverter : PropertyValueConverterBase, IDeliveryApiPropertyValueConverter { private readonly IMemberService _memberService; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IPublishedMemberCache _memberCache; public MemberPickerValueConverter( IMemberService memberService, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + IPublishedMemberCache memberCache) { _memberService = memberService; - _publishedSnapshotAccessor = publishedSnapshotAccessor; - _umbracoContextAccessor = umbracoContextAccessor; + _memberCache = memberCache; } public override bool IsConverter(IPublishedPropertyType propertyType) @@ -64,7 +61,6 @@ public class MemberPickerValueConverter : PropertyValueConverterBase, IDeliveryA } IPublishedContent? member; - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); if (source is int id) { IMember? m = _memberService.GetById(id); @@ -73,7 +69,7 @@ public class MemberPickerValueConverter : PropertyValueConverterBase, IDeliveryA return null; } - member = publishedSnapshot?.Members?.Get(m); + member = _memberCache.Get(m); if (member != null) { return member; @@ -92,7 +88,7 @@ public class MemberPickerValueConverter : PropertyValueConverterBase, IDeliveryA return null; } - member = publishedSnapshot?.Members?.Get(m); + member = _memberCache.Get(m); if (member != null) { diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs index 2d4060a89d..22e3067244 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs @@ -25,24 +25,29 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe }; private readonly IMemberService _memberService; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IApiContentBuilder _apiContentBuilder; private readonly IApiMediaBuilder _apiMediaBuilder; + private readonly IPublishedContentCache _contentCache; + private readonly IPublishedMediaCache _mediaCache; + private readonly IPublishedMemberCache _memberCache; public MultiNodeTreePickerValueConverter( - IPublishedSnapshotAccessor publishedSnapshotAccessor, IUmbracoContextAccessor umbracoContextAccessor, IMemberService memberService, IApiContentBuilder apiContentBuilder, - IApiMediaBuilder apiMediaBuilder) + IApiMediaBuilder apiMediaBuilder, + IPublishedContentCache contentCache, + IPublishedMediaCache mediaCache, + IPublishedMemberCache memberCache) { - _publishedSnapshotAccessor = publishedSnapshotAccessor ?? - throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); _umbracoContextAccessor = umbracoContextAccessor; _memberService = memberService; _apiContentBuilder = apiContentBuilder; _apiMediaBuilder = apiMediaBuilder; + _contentCache = contentCache; + _mediaCache = mediaCache; + _memberCache = memberCache; } public override bool IsConverter(IPublishedPropertyType propertyType) => @@ -95,7 +100,6 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe var multiNodeTreePicker = new List(); UmbracoObjectTypes objectType = UmbracoObjectTypes.Unknown; - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); foreach (Udi udi in udis) { if (udi is not GuidUdi guidUdi) @@ -111,14 +115,14 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe udi, ref objectType, UmbracoObjectTypes.Document, - id => publishedSnapshot.Content?.GetById(guidUdi.Guid)); + id => _contentCache.GetById(guidUdi.Guid)); break; case Constants.UdiEntityType.Media: multiNodeTreePickerItem = GetPublishedContent( udi, ref objectType, UmbracoObjectTypes.Media, - id => publishedSnapshot.Media?.GetById(guidUdi.Guid)); + id => _mediaCache.GetById(guidUdi.Guid)); break; case Constants.UdiEntityType.Member: multiNodeTreePickerItem = GetPublishedContent( @@ -133,7 +137,7 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe return null; } - IPublishedContent? member = publishedSnapshot?.Members?.Get(m); + IPublishedContent? member = _memberCache.Get(m); return member; }); break; @@ -188,8 +192,6 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe return DefaultValue(); } - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - var entityType = GetEntityType(propertyType); if (entityType == "content") @@ -203,14 +205,14 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe { Constants.UdiEntityType.Document => entityTypeUdis.Select(udi => { - IPublishedContent? content = publishedSnapshot.Content?.GetById(udi.Guid); + IPublishedContent? content = _contentCache.GetById(udi.Guid); return content != null ? _apiContentBuilder.Build(content) : null; }).WhereNotNull().ToArray(), Constants.UdiEntityType.Media => entityTypeUdis.Select(udi => { - IPublishedContent? media = publishedSnapshot.Media?.GetById(udi.Guid); + IPublishedContent? media = _mediaCache.GetById(udi.Guid); return media != null ? _apiMediaBuilder.Build(media) : null; diff --git a/src/Umbraco.Core/PublishedCache/IDatabaseCacheRebuilder.cs b/src/Umbraco.Core/PublishedCache/IDatabaseCacheRebuilder.cs new file mode 100644 index 0000000000..4eac53dc03 --- /dev/null +++ b/src/Umbraco.Core/PublishedCache/IDatabaseCacheRebuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.PublishedCache; + +public interface IDatabaseCacheRebuilder +{ + void Rebuild(); +} diff --git a/src/Umbraco.Core/PublishedCache/IPublishedCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedCache.cs index e4d8a2311c..2a8809deb6 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedCache.cs @@ -67,7 +67,7 @@ public interface IPublishedCache /// A culture. /// The contents. /// The value of overrides defaults. - [Obsolete] // FIXME: Remove when replacing nucache + [Obsolete("Scheduled for removal, use IDocumentNavigationQueryService instead in v17")] IEnumerable GetAtRoot(bool preview, string? culture = null); /// @@ -76,7 +76,7 @@ public interface IPublishedCache /// A culture. /// The contents. /// Considers published or unpublished content depending on defaults. - [Obsolete] // FIXME: Remove when replacing nucache + [Obsolete("Scheduled for removal, use IDocumentNavigationQueryService instead in v17")] IEnumerable GetAtRoot(string? culture = null); /// @@ -85,7 +85,7 @@ public interface IPublishedCache /// A value indicating whether to consider unpublished content. /// A value indicating whether the cache contains published content. /// The value of overrides defaults. - [Obsolete] // FIXME: Remove when replacing nucache + [Obsolete("Scheduled for removal in v17")] bool HasContent(bool preview); /// @@ -93,7 +93,7 @@ public interface IPublishedCache /// /// A value indicating whether the cache contains published content. /// Considers published or unpublished content depending on defaults. - [Obsolete] // FIXME: Remove when replacing nucache + [Obsolete("Scheduled for removal in v17")] bool HasContent(); /// @@ -118,7 +118,7 @@ public interface IPublishedCache /// /// The content type. /// The contents. - [Obsolete] // FIXME: Remove when replacing nucache + [Obsolete] IEnumerable GetByContentType(IPublishedContentType contentType); /// diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshot.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshot.cs deleted file mode 100644 index 43e6291701..0000000000 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshot.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Umbraco.Cms.Core.Cache; - -namespace Umbraco.Cms.Core.PublishedCache; - -/// -/// Specifies a published snapshot. -/// -/// -/// A published snapshot is a point-in-time capture of the current state of -/// everything that is "published". -/// -public interface IPublishedSnapshot : IDisposable -{ - /// - /// Gets the . - /// - IPublishedContentCache? Content { get; } - - /// - /// Gets the . - /// - IPublishedMediaCache? Media { get; } - - /// - /// Gets the . - /// - IPublishedMemberCache? Members { get; } - - /// - /// Gets the . - /// - IDomainCache? Domains { get; } - - /// - /// Gets the snapshot-level cache. - /// - /// - /// The snapshot-level cache belongs to this snapshot only. - /// - IAppCache? SnapshotCache { get; } - - /// - /// Gets the elements-level cache. - /// - /// - /// - /// The elements-level cache is shared by all snapshots relying on the same elements, - /// ie all snapshots built on top of unchanging content / media / etc. - /// - /// - IAppCache? ElementsCache { get; } - - /// - /// Forces the preview mode. - /// - /// The forced preview mode. - /// A callback to execute when reverting to previous preview. - /// - /// - /// Forcing to false means no preview. Forcing to true means 'full' preview if the snapshot is not already - /// previewing; - /// otherwise the snapshot keeps previewing according to whatever settings it is using already. - /// - /// Stops forcing preview when disposed. - /// - IDisposable ForcedPreview(bool preview, Action? callback = null); -} diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs deleted file mode 100644 index 8abc0906ce..0000000000 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Umbraco.Cms.Core.PublishedCache; - -/// -/// Provides access to a TryGetPublishedSnapshot bool method that will return true if the "current" -/// is not null. -/// -public interface IPublishedSnapshotAccessor -{ - bool TryGetPublishedSnapshot([NotNullWhen(true)] out IPublishedSnapshot? publishedSnapshot); -} diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs deleted file mode 100644 index 8e661aa758..0000000000 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs +++ /dev/null @@ -1,132 +0,0 @@ -using Umbraco.Cms.Core.Cache; - -namespace Umbraco.Cms.Core.PublishedCache; - -/// -/// Creates and manages instances. -/// -public interface IPublishedSnapshotService : IDisposable -{ - /* Various places (such as Node) want to access the XML content, today as an XmlDocument - * but to migrate to a new cache, they're migrating to an XPathNavigator. Still, they need - * to find out how to get that navigator. - * - * Because a cache such as NuCache is contextual i.e. it has a "snapshot" thing and remains - * consistent over the snapshot, the navigator has to come from the "current" snapshot. - * - * So although everything should be injected... we also need a notion of "the current published - * snapshot". This is provided by the IPublishedSnapshotAccessor. - * - */ - - /// - /// Creates a published snapshot. - /// - /// A preview token, or null if not previewing. - /// A published snapshot. - /// - /// If is null, the snapshot is not previewing, else it - /// is previewing, and what is or is not visible in preview depends on the content of the token, - /// which is not specified and depends on the actual published snapshot service implementation. - /// - IPublishedSnapshot CreatePublishedSnapshot(string? previewToken); - - /// - /// Rebuilds internal database caches (but does not reload). - /// - /// - /// If not null will process content for the matching content types, if empty will process all - /// content - /// - /// - /// If not null will process content for the matching media types, if empty will process all - /// media - /// - /// - /// If not null will process content for the matching members types, if empty will process all - /// members - /// - /// - /// - /// Forces the snapshot service to rebuild its internal database caches. For instance, some caches - /// may rely on a database table to store pre-serialized version of documents. - /// - /// - /// This does *not* reload the caches. Caches need to be reloaded, for instance via - /// RefreshAllPublishedSnapshot method. - /// - /// - void Rebuild( - IReadOnlyCollection? contentTypeIds = null, - IReadOnlyCollection? mediaTypeIds = null, - IReadOnlyCollection? memberTypeIds = null); - - - /// - /// Rebuilds all internal database caches (but does not reload). - /// - /// - /// - /// Forces the snapshot service to rebuild its internal database caches. For instance, some caches - /// may rely on a database table to store pre-serialized version of documents. - /// - /// - /// This does *not* reload the caches. Caches need to be reloaded, for instance via - /// RefreshAllPublishedSnapshot method. - /// - /// - void RebuildAll() => Rebuild(Array.Empty(), Array.Empty(), Array.Empty()); - - /* An IPublishedCachesService implementation can rely on transaction-level events to update - * its internal, database-level data, as these events are purely internal. However, it cannot - * rely on cache refreshers CacheUpdated events to update itself, as these events are external - * and the order-of-execution of the handlers cannot be guaranteed, which means that some - * user code may run before Umbraco is finished updating itself. Instead, the cache refreshers - * explicitly notify the service of changes. - * - */ - - /// - /// Notifies of content cache refresher changes. - /// - /// The changes. - /// A value indicating whether draft contents have been changed in the cache. - /// A value indicating whether published contents have been changed in the cache. - void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged); - - /// - /// Notifies of media cache refresher changes. - /// - /// The changes. - /// A value indicating whether medias have been changed in the cache. - void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged); - - // there is no NotifyChanges for MemberCacheRefresher because we're not caching members. - - /// - /// Notifies of content type refresher changes. - /// - /// The changes. - void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads); - - /// - /// Notifies of data type refresher changes. - /// - /// The changes. - void Notify(DataTypeCacheRefresher.JsonPayload[] payloads); - - /// - /// Notifies of domain refresher changes. - /// - /// The changes. - void Notify(DomainCacheRefresher.JsonPayload[] payloads); - - /// - /// Cleans up unused snapshots - /// - Task CollectAsync(); - - void ResetLocalDb() - { - } -} diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotStatus.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotStatus.cs deleted file mode 100644 index 1ae08cc42d..0000000000 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotStatus.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Umbraco.Cms.Core.PublishedCache; - -/// -/// Returns the currents status for nucache -/// -public interface IPublishedSnapshotStatus -{ - /// - /// Gets the status report as a string - /// - string GetStatus(); -} diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs index f4e381d3a1..f8bffbba77 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs @@ -1,5 +1,8 @@ using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PublishedCache.Internal; @@ -66,13 +69,18 @@ public sealed class InternalPublishedContent : IPublishedContent public PublishedItemType ItemType => PublishedItemType.Content; - public IPublishedContent? Parent { get; set; } + [Obsolete("Please use TryGetParentKey() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")] + public IPublishedContent? Parent => this.Parent(StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()); public bool IsDraft(string? culture = null) => false; public bool IsPublished(string? culture = null) => true; - public IEnumerable Children { get; set; } = Enumerable.Empty(); + [Obsolete("Please use TryGetChildrenKeys() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")] + public IEnumerable Children => this.Children( + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()); public IEnumerable ChildrenForAllCultures => Children; @@ -94,7 +102,7 @@ public sealed class InternalPublishedContent : IPublishedContent IPublishedContent? content = this; while (content != null && (property == null || property.HasValue() == false)) { - content = content.Parent; + content = content.Parent(StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()); property = content?.GetProperty(alias); } diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs deleted file mode 100644 index 5b57236d4f..0000000000 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.ComponentModel; -using Umbraco.Cms.Core.Models.PublishedContent; - -namespace Umbraco.Cms.Core.PublishedCache.Internal; - -// TODO: Only used in unit tests, needs to be moved to test project -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed class InternalPublishedContentCache : PublishedCacheBase, IPublishedContentCache, IPublishedMediaCache -{ - private readonly Dictionary _content = new(); - - public InternalPublishedContentCache() - : base(false) - { - } - - public Task GetByIdAsync(int id, bool preview = false) => throw new NotImplementedException(); - - public Task GetByIdAsync(Guid key, bool preview = false) => throw new NotImplementedException(); - - public Task HasByIdAsync(int id, bool preview = false) => throw new NotImplementedException(); - - public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException(); - - public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) => - throw new NotImplementedException(); - - public string GetRouteById(bool preview, int contentId, string? culture = null) => - throw new NotImplementedException(); - - public string GetRouteById(int contentId, string? culture = null) => throw new NotImplementedException(); - - public override IPublishedContent? GetById(bool preview, int contentId) => - _content.ContainsKey(contentId) ? _content[contentId] : null; - - public override IPublishedContent GetById(bool preview, Guid contentId) => throw new NotImplementedException(); - - public override IPublishedContent GetById(bool preview, Udi nodeId) => throw new NotSupportedException(); - - public override bool HasById(bool preview, int contentId) => _content.ContainsKey(contentId); - - public override IEnumerable GetAtRoot(bool preview, string? culture = null) => - _content.Values.Where(x => x.Parent == null); - - public override bool HasContent(bool preview) => _content.Count > 0; - - public override IPublishedContentType GetContentType(int id) => throw new NotImplementedException(); - - public override IPublishedContentType GetContentType(string alias) => throw new NotImplementedException(); - - public override IPublishedContentType GetContentType(Guid key) => throw new NotImplementedException(); - - public override IEnumerable GetByContentType(IPublishedContentType contentType) => - throw new NotImplementedException(); - - // public void Add(InternalPublishedContent content) => _content[content.Id] = content.CreateModel(Mock.Of()); - public void Clear() => _content.Clear(); - public Task GetByIdAsync(int id) => throw new NotImplementedException(); - - public Task GetByKeyAsync(Guid key) => throw new NotImplementedException(); - - public Task HasByIdAsync(int id) => throw new NotImplementedException(); -} diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs deleted file mode 100644 index 015962b5aa..0000000000 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.ComponentModel; -using Umbraco.Cms.Core.Cache; - -namespace Umbraco.Cms.Core.PublishedCache.Internal; - -// TODO: Only used in unit tests, needs to be moved to test project -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed class InternalPublishedSnapshot : IPublishedSnapshot -{ - public InternalPublishedContentCache InnerContentCache { get; } = new(); - - public InternalPublishedContentCache InnerMediaCache { get; } = new(); - - public IPublishedContentCache Content => InnerContentCache; - - public IPublishedMediaCache Media => InnerMediaCache; - - public IPublishedMemberCache? Members => null; - - public IDomainCache? Domains => null; - - public IAppCache? SnapshotCache => null; - - public IDisposable ForcedPreview(bool forcedPreview, Action? callback = null) => - throw new NotImplementedException(); - - public IAppCache? ElementsCache => null; - - public void Dispose() - { - } - - public void Resync() - { - } -} diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs deleted file mode 100644 index 09de76ace5..0000000000 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.ComponentModel; -using Umbraco.Cms.Core.Cache; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Core.PublishedCache.Internal; - -// TODO: Only used in unit tests, needs to be moved to test project -[EditorBrowsable(EditorBrowsableState.Never)] -public class InternalPublishedSnapshotService : IPublishedSnapshotService -{ - private InternalPublishedSnapshot? _previewSnapshot; - private InternalPublishedSnapshot? _snapshot; - - public Task CollectAsync() => Task.CompletedTask; - - public IPublishedSnapshot CreatePublishedSnapshot(string? previewToken) - { - if (previewToken.IsNullOrWhiteSpace()) - { - return _snapshot ??= new InternalPublishedSnapshot(); - } - - return _previewSnapshot ??= new InternalPublishedSnapshot(); - } - - public void Dispose() - { - _snapshot?.Dispose(); - _previewSnapshot?.Dispose(); - } - - public void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged) - { - draftChanged = false; - publishedChanged = false; - } - - public void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged) => anythingChanged = false; - - public void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads) - { - } - - public void Notify(DataTypeCacheRefresher.JsonPayload[] payloads) - { - } - - public void Notify(DomainCacheRefresher.JsonPayload[] payloads) - { - } - - public void Rebuild(IReadOnlyCollection? contentTypeIds = null, IReadOnlyCollection? mediaTypeIds = null, IReadOnlyCollection? memberTypeIds = null) - { - } -} diff --git a/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs index 88acd5d29c..2abba65d4a 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs @@ -64,11 +64,5 @@ public abstract class PublishedCacheBase : IPublishedCache public abstract IPublishedContentType? GetContentType(Guid key); - public virtual IEnumerable GetByContentType(IPublishedContentType contentType) => - - // this is probably not super-efficient, but works - // some cache implementation may want to override it, though - GetAtRoot() - .SelectMany(x => x.DescendantsOrSelf(_variationContextAccessor!)) - .Where(x => x.ContentType.Id == contentType.Id); + public virtual IEnumerable GetByContentType(IPublishedContentType contentType) => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/PublishedCache/PublishedElement.cs b/src/Umbraco.Core/PublishedCache/PublishedElement.cs index 297a62b589..92d8646539 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedElement.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedElement.cs @@ -18,7 +18,7 @@ public class PublishedElement : IPublishedElement // initializes a new instance of the PublishedElement class // within the context of a published snapshot service (eg a published content property value) - public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary? values, bool previewing, PropertyCacheLevel referenceCacheLevel, IPublishedSnapshotAccessor? publishedSnapshotAccessor) + public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary? values, bool previewing, PropertyCacheLevel referenceCacheLevel, ICacheManager? cacheManager) { if (key == Guid.Empty) { @@ -30,13 +30,6 @@ public class PublishedElement : IPublishedElement throw new ArgumentNullException(nameof(values)); } - if (referenceCacheLevel != PropertyCacheLevel.None && publishedSnapshotAccessor == null) - { - throw new ArgumentNullException( - "A published snapshot accessor is required when referenceCacheLevel != None.", - nameof(publishedSnapshotAccessor)); - } - ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); Key = key; @@ -47,7 +40,7 @@ public class PublishedElement : IPublishedElement .Select(propertyType => { values.TryGetValue(propertyType.Alias, out var value); - return (IPublishedProperty)new PublishedElementPropertyBase(propertyType, this, previewing, referenceCacheLevel, value, publishedSnapshotAccessor); + return (IPublishedProperty)new PublishedElementPropertyBase(propertyType, this, previewing, referenceCacheLevel,cacheManager, value); }) .ToArray() ?? new IPublishedProperty[0]; diff --git a/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs b/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs index 53e8156538..0452cf0b03 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs @@ -15,10 +15,10 @@ internal class PublishedElementPropertyBase : PublishedPropertyBase // so making it configurable. private const bool FullCacheWhenPreviewing = true; private readonly object _locko = new(); - private readonly IPublishedSnapshotAccessor? _publishedSnapshotAccessor; private readonly object? _sourceValue; protected readonly bool IsMember; protected readonly bool IsPreviewing; + private readonly ICacheManager? _cacheManager; private CacheValues? _cacheValues; private bool _interInitialized; @@ -30,14 +30,14 @@ internal class PublishedElementPropertyBase : PublishedPropertyBase IPublishedElement element, bool previewing, PropertyCacheLevel referenceCacheLevel, - object? sourceValue = null, - IPublishedSnapshotAccessor? publishedSnapshotAccessor = null) + ICacheManager? cacheManager, + object? sourceValue = null) : base(propertyType, referenceCacheLevel) { _sourceValue = sourceValue; - _publishedSnapshotAccessor = publishedSnapshotAccessor; Element = element; IsPreviewing = previewing; + _cacheManager = cacheManager; IsMember = propertyType.ContentType?.ItemType == PublishedItemType.Member; } @@ -118,33 +118,13 @@ internal class PublishedElementPropertyBase : PublishedPropertyBase } } - private IAppCache? GetSnapshotCache() - { - // cache within the snapshot cache, unless previewing, then use the snapshot or - // elements cache (if we don't want to pollute the elements cache with short-lived - // data) depending on settings - // for members, always cache in the snapshot cache - never pollute elements cache - if (_publishedSnapshotAccessor is null) - { - return null; - } - - if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)) - { - return null; - } - - return (IsPreviewing == false || FullCacheWhenPreviewing) && IsMember == false - ? publishedSnapshot!.ElementsCache - : publishedSnapshot!.SnapshotCache; - } - private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) { CacheValues cacheValues; switch (cacheLevel) { case PropertyCacheLevel.None: + case PropertyCacheLevel.Snapshot: // never cache anything cacheValues = new CacheValues(); break; @@ -153,17 +133,7 @@ internal class PublishedElementPropertyBase : PublishedPropertyBase cacheValues = _cacheValues ??= new CacheValues(); break; case PropertyCacheLevel.Elements: - // cache within the elements cache, depending... - IAppCache? snapshotCache = GetSnapshotCache(); - cacheValues = (CacheValues?)snapshotCache?.Get(ValuesCacheKey, () => new CacheValues()) ?? - new CacheValues(); - break; - case PropertyCacheLevel.Snapshot: - IPublishedSnapshot? publishedSnapshot = _publishedSnapshotAccessor?.GetRequiredPublishedSnapshot(); - - // cache within the snapshot cache - IAppCache? facadeCache = publishedSnapshot?.SnapshotCache; - cacheValues = (CacheValues?)facadeCache?.Get(ValuesCacheKey, () => new CacheValues()) ?? + cacheValues = (CacheValues?)_cacheManager?.ElementsCache.Get(ValuesCacheKey, () => new CacheValues()) ?? new CacheValues(); break; default: diff --git a/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs b/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs deleted file mode 100644 index 91e32e6db4..0000000000 --- a/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Umbraco.Cms.Core.Web; - -namespace Umbraco.Cms.Core.PublishedCache; - -// TODO: This is a mess. This is a circular reference: -// IPublishedSnapshotAccessor -> PublishedSnapshotService -> UmbracoContext -> PublishedSnapshotService -> IPublishedSnapshotAccessor -// Injecting IPublishedSnapshotAccessor into PublishedSnapshotService seems pretty strange -// The underlying reason for this mess is because IPublishedContent is both a service and a model. -// Until that is fixed, IPublishedContent will need to have a IPublishedSnapshotAccessor -public class UmbracoContextPublishedSnapshotAccessor : IPublishedSnapshotAccessor -{ - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - - public UmbracoContextPublishedSnapshotAccessor(IUmbracoContextAccessor umbracoContextAccessor) => - _umbracoContextAccessor = umbracoContextAccessor; - - public IPublishedSnapshot? PublishedSnapshot - { - get - { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) - { - return null; - } - - return umbracoContext?.PublishedSnapshot; - } - - set => throw new NotSupportedException(); // not ok to set - } - - public bool TryGetPublishedSnapshot([NotNullWhen(true)] out IPublishedSnapshot? publishedSnapshot) - { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) - { - publishedSnapshot = null; - return false; - } - - publishedSnapshot = umbracoContext?.PublishedSnapshot; - - return publishedSnapshot is not null; - } -} diff --git a/src/Umbraco.Core/Routing/AliasUrlProvider.cs b/src/Umbraco.Core/Routing/AliasUrlProvider.cs index 65d9387e2e..59e9e1d381 100644 --- a/src/Umbraco.Core/Routing/AliasUrlProvider.cs +++ b/src/Umbraco.Core/Routing/AliasUrlProvider.cs @@ -1,6 +1,10 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -14,6 +18,8 @@ public class AliasUrlProvider : IUrlProvider private readonly IPublishedValueFallback _publishedValueFallback; private readonly ISiteDomainMapper _siteDomainMapper; private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IPublishedContentCache _contentCache; + private readonly IDocumentNavigationQueryService _navigationQueryService; private readonly UriUtility _uriUtility; private RequestHandlerSettings _requestConfig; @@ -22,17 +28,39 @@ public class AliasUrlProvider : IUrlProvider ISiteDomainMapper siteDomainMapper, UriUtility uriUtility, IPublishedValueFallback publishedValueFallback, - IUmbracoContextAccessor umbracoContextAccessor) + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) { _requestConfig = requestConfig.CurrentValue; _siteDomainMapper = siteDomainMapper; _uriUtility = uriUtility; _publishedValueFallback = publishedValueFallback; _umbracoContextAccessor = umbracoContextAccessor; + _contentCache = contentCache; + _navigationQueryService = navigationQueryService; requestConfig.OnChange(x => _requestConfig = x); } + [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + public AliasUrlProvider( + IOptionsMonitor requestConfig, + ISiteDomainMapper siteDomainMapper, + UriUtility uriUtility, + IPublishedValueFallback publishedValueFallback, + IUmbracoContextAccessor umbracoContextAccessor) + : this( + requestConfig, + siteDomainMapper, + uriUtility, + publishedValueFallback, + umbracoContextAccessor, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } + // note - at the moment we seem to accept pretty much anything as an alias // without any form of validation ... could even prob. kill the XPath ... // ok, this is somewhat experimental and is NOT enabled by default @@ -74,16 +102,16 @@ public class AliasUrlProvider : IUrlProvider // look for domains, walking up the tree IPublishedContent? n = node; - IEnumerable? domainUris = DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); + IEnumerable? domainUris = DomainUtilities.DomainsForNode(umbracoContext.Domains, _siteDomainMapper, n.Id, current, false); // n is null at root while (domainUris == null && n != null) { // move to parent node - n = n.Parent; + n = n.Parent(_contentCache, _navigationQueryService); domainUris = n == null ? null - : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); + : DomainUtilities.DomainsForNode(umbracoContext.Domains, _siteDomainMapper, n.Id, current, false); } // determine whether the alias property varies diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs b/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs index 3a04c2cb5b..f592fb0a0f 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -21,6 +22,8 @@ public class ContentFinderByUrlAlias : IContentFinder private readonly ILogger _logger; private readonly IPublishedValueFallback _publishedValueFallback; private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IPublishedContentCache _contentCache; + private readonly IDocumentNavigationQueryService _documentNavigationQueryService; private readonly IVariationContextAccessor _variationContextAccessor; /// @@ -30,11 +33,15 @@ public class ContentFinderByUrlAlias : IContentFinder ILogger logger, IPublishedValueFallback publishedValueFallback, IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService documentNavigationQueryService) { _publishedValueFallback = publishedValueFallback; _variationContextAccessor = variationContextAccessor; _umbracoContextAccessor = umbracoContextAccessor; + _contentCache = contentCache; + _documentNavigationQueryService = documentNavigationQueryService; _logger = logger; } @@ -138,14 +145,14 @@ public class ContentFinderByUrlAlias : IContentFinder if (rootNodeId > 0) { IPublishedContent? rootNode = cache?.GetById(rootNodeId); - return rootNode?.Descendants(_variationContextAccessor).FirstOrDefault(x => IsMatch(x, test1, test2)); + return rootNode?.Descendants(_variationContextAccessor, _contentCache, _documentNavigationQueryService).FirstOrDefault(x => IsMatch(x, test1, test2)); } if (cache is not null) { foreach (IPublishedContent rootContent in cache.GetAtRoot()) { - IPublishedContent? c = rootContent.DescendantsOrSelf(_variationContextAccessor) + IPublishedContent? c = rootContent.DescendantsOrSelf(_variationContextAccessor, _contentCache, _documentNavigationQueryService) .FirstOrDefault(x => IsMatch(x, test1, test2)); if (c != null) { diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs index 4a5d5d2826..72f1dac37e 100644 --- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs @@ -5,7 +5,9 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -21,27 +23,53 @@ public class DefaultUrlProvider : IUrlProvider private readonly ILogger _logger; private readonly ISiteDomainMapper _siteDomainMapper; private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IPublishedContentCache _contentCache; + private readonly IDocumentNavigationQueryService _navigationQueryService; private readonly UriUtility _uriUtility; private RequestHandlerSettings _requestSettings; - public DefaultUrlProvider( - IOptionsMonitor requestSettings, - ILogger logger, - ISiteDomainMapper siteDomainMapper, - IUmbracoContextAccessor umbracoContextAccessor, - UriUtility uriUtility, - ILocalizationService localizationService) - { - _requestSettings = requestSettings.CurrentValue; - _logger = logger; - _siteDomainMapper = siteDomainMapper; - _umbracoContextAccessor = umbracoContextAccessor; - _uriUtility = uriUtility; - _localizationService = localizationService; + public DefaultUrlProvider( + IOptionsMonitor requestSettings, + ILogger logger, + ISiteDomainMapper siteDomainMapper, + IUmbracoContextAccessor umbracoContextAccessor, + UriUtility uriUtility, + ILocalizationService localizationService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) + { + _requestSettings = requestSettings.CurrentValue; + _logger = logger; + _siteDomainMapper = siteDomainMapper; + _umbracoContextAccessor = umbracoContextAccessor; + _uriUtility = uriUtility; + _localizationService = localizationService; + _contentCache = contentCache; + _navigationQueryService = navigationQueryService; requestSettings.OnChange(x => _requestSettings = x); } + [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + public DefaultUrlProvider( + IOptionsMonitor requestSettings, + ILogger logger, + ISiteDomainMapper siteDomainMapper, + IUmbracoContextAccessor umbracoContextAccessor, + UriUtility uriUtility, + ILocalizationService localizationService) + : this( + requestSettings, + logger, + siteDomainMapper, + umbracoContextAccessor, + uriUtility, + localizationService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } + #region GetOtherUrls /// @@ -68,15 +96,15 @@ public class DefaultUrlProvider : IUrlProvider // look for domains, walking up the tree IPublishedContent? n = node; IEnumerable? domainUris = - DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); + DomainUtilities.DomainsForNode(umbracoContext.Domains, _siteDomainMapper, n.Id, current, false); // n is null at root while (domainUris == null && n != null) { - n = n.Parent; // move to parent node + n = n.Parent(_contentCache, _navigationQueryService); // move to parent node domainUris = n == null ? null - : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current); + : DomainUtilities.DomainsForNode(umbracoContext.Domains, _siteDomainMapper, n.Id, current); } // no domains = exit @@ -152,7 +180,7 @@ public class DefaultUrlProvider : IUrlProvider DomainAndUri? domainUri = pos == 0 ? null : DomainUtilities.DomainForNode( - umbracoContext.PublishedSnapshot.Domains, + umbracoContext.Domains, _siteDomainMapper, int.Parse(route[..pos], CultureInfo.InvariantCulture), current, diff --git a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs index c4fa6cfe1d..4e636a3f9e 100644 --- a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -21,6 +22,7 @@ public class NewDefaultUrlProvider : IUrlProvider private readonly IDomainCache _domainCache; private readonly IIdKeyMap _idKeyMap; private readonly IDocumentUrlService _documentUrlService; + private readonly IDocumentNavigationQueryService _navigationQueryService; private readonly ILocalizedTextService? _localizedTextService; private readonly ILogger _logger; private readonly ISiteDomainMapper _siteDomainMapper; @@ -28,30 +30,32 @@ public class NewDefaultUrlProvider : IUrlProvider private readonly UriUtility _uriUtility; private RequestHandlerSettings _requestSettings; - public NewDefaultUrlProvider( - IOptionsMonitor requestSettings, - ILogger logger, - ISiteDomainMapper siteDomainMapper, - IUmbracoContextAccessor umbracoContextAccessor, - UriUtility uriUtility, - ILocalizationService localizationService, - IPublishedContentCache publishedContentCache, - IDomainCache domainCache, - IIdKeyMap idKeyMap, - IDocumentUrlService documentUrlService) - { - _requestSettings = requestSettings.CurrentValue; - _logger = logger; - _siteDomainMapper = siteDomainMapper; - _umbracoContextAccessor = umbracoContextAccessor; - _uriUtility = uriUtility; - _localizationService = localizationService; - _publishedContentCache = publishedContentCache; - _domainCache = domainCache; - _idKeyMap = idKeyMap; - _documentUrlService = documentUrlService; + public NewDefaultUrlProvider( + IOptionsMonitor requestSettings, + ILogger logger, + ISiteDomainMapper siteDomainMapper, + IUmbracoContextAccessor umbracoContextAccessor, + UriUtility uriUtility, + ILocalizationService localizationService, + IPublishedContentCache publishedContentCache, + IDomainCache domainCache, + IIdKeyMap idKeyMap, + IDocumentUrlService documentUrlService, + IDocumentNavigationQueryService navigationQueryService) + { + _requestSettings = requestSettings.CurrentValue; + _logger = logger; + _siteDomainMapper = siteDomainMapper; + _umbracoContextAccessor = umbracoContextAccessor; + _uriUtility = uriUtility; + _localizationService = localizationService; + _publishedContentCache = publishedContentCache; + _domainCache = domainCache; + _idKeyMap = idKeyMap; + _documentUrlService = documentUrlService; + _navigationQueryService = navigationQueryService; - requestSettings.OnChange(x => _requestSettings = x); + requestSettings.OnChange(x => _requestSettings = x); } #region GetOtherUrls @@ -95,7 +99,7 @@ public class NewDefaultUrlProvider : IUrlProvider // n is null at root while (domainUris == null && n != null) { - n = n.Parent; // move to parent node + n = n.Parent(_publishedContentCache, _navigationQueryService); // move to parent node domainUris = n == null ? null : DomainUtilities.DomainsForNode(_domainCache, _siteDomainMapper, n.Id, current); diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index 28cd4323eb..afde85a13d 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -301,7 +301,7 @@ public class PublishedRouter : IPublishedRouter } IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - IDomainCache? domainsCache = umbracoContext.PublishedSnapshot.Domains; + IDomainCache? domainsCache = umbracoContext.Domains; var domains = domainsCache?.GetAll(false).ToList(); // determines whether a domain corresponds to a published document, since some @@ -311,7 +311,7 @@ public class PublishedRouter : IPublishedRouter bool IsPublishedContentDomain(Domain domain) { // just get it from content cache - optimize there, not here - IPublishedContent? domainDocument = umbracoContext.PublishedSnapshot.Content?.GetById(domain.ContentId); + IPublishedContent? domainDocument = umbracoContext.Content?.GetById(domain.ContentId); // not published - at all if (domainDocument == null) diff --git a/src/Umbraco.Core/Routing/UrlProvider.cs b/src/Umbraco.Core/Routing/UrlProvider.cs index 067c748da1..f40c240a73 100644 --- a/src/Umbraco.Core/Routing/UrlProvider.cs +++ b/src/Umbraco.Core/Routing/UrlProvider.cs @@ -1,6 +1,10 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -21,20 +25,50 @@ namespace Umbraco.Cms.Core.Routing /// The list of URL providers. /// The list of media URL providers. /// The current variation accessor. - public UrlProvider(IUmbracoContextAccessor umbracoContextAccessor, IOptions routingSettings, UrlProviderCollection urlProviders, MediaUrlProviderCollection mediaUrlProviders, IVariationContextAccessor variationContextAccessor) + /// The content cache. + /// The query service for the in-memory navigation structure. + public UrlProvider( + IUmbracoContextAccessor umbracoContextAccessor, + IOptions routingSettings, + UrlProviderCollection urlProviders, + MediaUrlProviderCollection mediaUrlProviders, + IVariationContextAccessor variationContextAccessor, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) { _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); _urlProviders = urlProviders; _mediaUrlProviders = mediaUrlProviders; _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + _contentCache = contentCache; + _navigationQueryService = navigationQueryService; Mode = routingSettings.Value.UrlProviderMode; + } + [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + public UrlProvider( + IUmbracoContextAccessor umbracoContextAccessor, + IOptions routingSettings, + UrlProviderCollection urlProviders, + MediaUrlProviderCollection mediaUrlProviders, + IVariationContextAccessor variationContextAccessor) + : this( + umbracoContextAccessor, + routingSettings, + urlProviders, + mediaUrlProviders, + variationContextAccessor, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { } private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IEnumerable _urlProviders; private readonly IEnumerable _mediaUrlProviders; private readonly IVariationContextAccessor _variationContextAccessor; + private readonly IPublishedContentCache _contentCache; + private readonly IDocumentNavigationQueryService _navigationQueryService; /// /// Gets or sets the provider URL mode. @@ -113,7 +147,7 @@ namespace Umbraco.Cms.Core.Routing // be nice with tests, assume things can be null, ultimately fall back to invariant // (but only for variant content of course) // We need to check all ancestors because urls are variant even for invariant content, if an ancestor is variant. - if (culture == null && content.AncestorsOrSelf().Any(x => x.ContentType.VariesByCulture())) + if (culture == null && content.AncestorsOrSelf(_contentCache, _navigationQueryService).Any(x => x.ContentType.VariesByCulture())) { culture = _variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } diff --git a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs index 1c86502cd1..6606ff8f6c 100644 --- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs @@ -4,20 +4,22 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Web; namespace Umbraco.Extensions; public static class UrlProviderExtensions { - [Obsolete("Use GetContentUrlsAsync that takes ILanguageService instead of ILocalizationService. Will be removed in V15.")] + [Obsolete("Use GetContentUrlsAsync that takes all parameters. Will be removed in V17.")] public static async Task> GetContentUrlsAsync( this IContent content, IPublishedRouter publishedRouter, IUmbracoContext umbracoContext, - ILocalizationService localizationService, + ILanguageService languageService, ILocalizedTextService textService, IContentService contentService, IVariationContextAccessor variationContextAccessor, @@ -27,13 +29,15 @@ public static class UrlProviderExtensions => await content.GetContentUrlsAsync( publishedRouter, umbracoContext, - StaticServiceProvider.Instance.GetRequiredService(), + languageService, textService, contentService, variationContextAccessor, logger, uriUtility, - publishedUrlProvider); + publishedUrlProvider, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()); /// /// Gets the URLs of the content item. @@ -52,7 +56,9 @@ public static class UrlProviderExtensions IVariationContextAccessor variationContextAccessor, ILogger logger, UriUtility uriUtility, - IPublishedUrlProvider publishedUrlProvider) + IPublishedUrlProvider publishedUrlProvider, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) { ArgumentNullException.ThrowIfNull(content); ArgumentNullException.ThrowIfNull(publishedRouter); @@ -89,7 +95,7 @@ public static class UrlProviderExtensions // get all URLs for all cultures // in a HashSet, so de-duplicates too - foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider)) + foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider, contentCache, navigationQueryService)) { urls.Add(cultureUrl); } @@ -139,7 +145,9 @@ public static class UrlProviderExtensions IVariationContextAccessor variationContextAccessor, ILogger logger, UriUtility uriUtility, - IPublishedUrlProvider publishedUrlProvider) + IPublishedUrlProvider publishedUrlProvider, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) { var result = new List(); @@ -178,7 +186,7 @@ public static class UrlProviderExtensions // got a URL, deal with collisions, add URL default: // detect collisions, etc - Attempt hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility); + Attempt hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility, contentCache, navigationQueryService); if (hasCollision.Success && hasCollision.Result is not null) { result.Add(hasCollision.Result); @@ -234,7 +242,9 @@ public static class UrlProviderExtensions IPublishedRouter publishedRouter, ILocalizedTextService textService, IVariationContextAccessor variationContextAccessor, - UriUtility uriUtility) + UriUtility uriUtility, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) { // test for collisions on the 'main' URL var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); @@ -273,7 +283,7 @@ public static class UrlProviderExtensions while (o != null) { l.Add(o.Name(variationContextAccessor)!); - o = o.Parent; + o = o.Parent(contentCache, navigationQueryService); } l.Reverse(); diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs index 1b69029fd4..5f9f6a759f 100644 --- a/src/Umbraco.Core/Services/DocumentUrlService.cs +++ b/src/Umbraco.Core/Services/DocumentUrlService.cs @@ -473,6 +473,12 @@ public class DocumentUrlService : IDocumentUrlService return GetFullUrl(isRootFirstItem, urlSegments, null); } + public bool HasAny() + { + ThrowIfNotInitialized(); + return _cache.Any(); + } + public async Task> ListUrlsAsync(Guid contentKey) { @@ -489,8 +495,7 @@ public class DocumentUrlService : IDocumentUrlService .Concat(_contentService.GetAncestors(documentIdAttempt.Result).Select(x => x.Key).Reverse()); IEnumerable languages = await _languageService.GetAllAsync(); - IEnumerable cultures = languages.Select(x=>x.IsoCode); - + var cultures = languages.ToDictionary(x=>x.IsoCode); Guid[] ancestorsOrSelfKeysArray = ancestorsOrSelfKeys as Guid[] ?? ancestorsOrSelfKeys.ToArray(); Dictionary>> ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray.ToDictionary(x => x, async ancestorKey => @@ -499,7 +504,7 @@ public class DocumentUrlService : IDocumentUrlService return domains.ToDictionary(x => x.LanguageIsoCode!); }); - foreach (var culture in cultures) + foreach ((string culture, ILanguage language) in cultures) { var urlSegments = new List(); IDomain? foundDomain = null; @@ -527,6 +532,12 @@ public class DocumentUrlService : IDocumentUrlService } } + //If we did not find a domain and this is not the default language, then the content is not routable + if (foundDomain is null && language.IsDefault is false) + { + continue; + } + var isRootFirstItem = GetTopMostRootKey() == ancestorsOrSelfKeysArray.Last(); result.Add(new UrlInfo( text: GetFullUrl(isRootFirstItem, urlSegments, foundDomain), diff --git a/src/Umbraco.Core/Services/IDocumentUrlService.cs b/src/Umbraco.Core/Services/IDocumentUrlService.cs index 91427fa5f0..e5d2c0df7a 100644 --- a/src/Umbraco.Core/Services/IDocumentUrlService.cs +++ b/src/Umbraco.Core/Services/IDocumentUrlService.cs @@ -33,4 +33,6 @@ public interface IDocumentUrlService Task CreateOrUpdateUrlSegmentsWithDescendantsAsync(Guid key); Task CreateOrUpdateUrlSegmentsAsync(Guid key); string GetLegacyRouteFormat(Guid key, string? culture, bool isDraft); + + bool HasAny(); } diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index fb6c7a0381..a2cd8ea354 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -65,6 +65,28 @@ internal abstract class ContentNavigationServiceBase public bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable siblingsKeys) => TryGetSiblingsKeysFromStructure(_recycleBinNavigationStructure, key, out siblingsKeys); + public bool TryGetLevel(Guid contentKey, out int level) + { + level = 1; + Guid? parentKey; + if (TryGetParentKey(contentKey, out parentKey) is false) + { + return false; + } + + while (parentKey is not null) + { + if (TryGetParentKey(parentKey.Value, out parentKey) is false) + { + return false; + } + + level++; + } + + return true; + } + public bool MoveToBin(Guid key) { if (TryRemoveNodeFromParentInStructure(_navigationStructure, key, out NavigationNode? nodeToRemove) is false || nodeToRemove is null) diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index ad4e7ae150..e440c30794 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -44,4 +44,6 @@ public interface INavigationQueryService } bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys); + + bool TryGetLevel(Guid contentKey, out int level); } diff --git a/src/Umbraco.Core/Web/IUmbracoContext.cs b/src/Umbraco.Core/Web/IUmbracoContext.cs index 7c0bb311bf..a481d0352a 100644 --- a/src/Umbraco.Core/Web/IUmbracoContext.cs +++ b/src/Umbraco.Core/Web/IUmbracoContext.cs @@ -26,26 +26,21 @@ public interface IUmbracoContext : IDisposable /// That is, lowercase, no trailing slash after path, no .aspx... Uri CleanedUmbracoUrl { get; } - /// - /// Gets the published snapshot. - /// - IPublishedSnapshot PublishedSnapshot { get; } - // TODO: Obsolete these, and use cache manager to get /// /// Gets the published content cache. /// - IPublishedContentCache? Content { get; } + IPublishedContentCache Content { get; } /// /// Gets the published media cache. /// - IPublishedMediaCache? Media { get; } + IPublishedMediaCache Media { get; } /// /// Gets the domains cache. /// - IDomainCache? Domains { get; } + IDomainCache Domains { get; } /// /// Gets or sets the PublishedRequest object @@ -65,9 +60,10 @@ public interface IUmbracoContext : IDisposable /// bool InPreviewMode { get; } + // TODO: Do we need this? /// /// Forces the context into preview /// /// A instance to be disposed to exit the preview context - IDisposable ForcedPreview(bool preview); + // IDisposable ForcedPreview(bool preview); } diff --git a/src/Umbraco.Core/Webhooks/Events/Content/ContentEmptiedRecycleBinWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentEmptiedRecycleBinWebhookEvent.cs index e37455da5e..056ef37f01 100644 --- a/src/Umbraco.Core/Webhooks/Events/Content/ContentEmptiedRecycleBinWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentEmptiedRecycleBinWebhookEvent.cs @@ -12,7 +12,6 @@ namespace Umbraco.Cms.Core.Webhooks.Events; [WebhookEvent("Content Recycle Bin Emptied", Constants.WebhookEvents.Types.Content)] public class ContentEmptiedRecycleBinWebhookEvent : WebhookEventContentBase { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IApiContentBuilder _apiContentBuilder; public ContentEmptiedRecycleBinWebhookEvent( @@ -20,7 +19,6 @@ public class ContentEmptiedRecycleBinWebhookEvent : WebhookEventContentBase webhookSettings, IServerRoleAccessor serverRoleAccessor, - IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiContentBuilder apiContentBuilder) : base( webhookFiringService, @@ -28,7 +26,6 @@ public class ContentEmptiedRecycleBinWebhookEvent : WebhookEventContentBase { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IApiContentBuilder _apiContentBuilder; + private readonly IPublishedContentCache _publishedContentCache; public ContentPublishedWebhookEvent( IWebhookFiringService webhookFiringService, IWebhookService webhookService, IOptionsMonitor webhookSettings, IServerRoleAccessor serverRoleAccessor, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IApiContentBuilder apiContentBuilder) + IApiContentBuilder apiContentBuilder, + IPublishedContentCache publishedContentCache) : base( webhookFiringService, webhookService, webhookSettings, serverRoleAccessor) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; _apiContentBuilder = apiContentBuilder; + _publishedContentCache = publishedContentCache; } public override string Alias => Constants.WebhookEvents.Aliases.ContentPublish; @@ -39,12 +39,7 @@ public class ContentPublishedWebhookEvent : WebhookEventContentBase { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IPublishedContentCache _contentCache; private readonly IApiContentBuilder _apiContentBuilder; public ContentRolledBackWebhookEvent( @@ -21,7 +21,7 @@ public class ContentRolledBackWebhookEvent : WebhookEventContentBase webhookSettings, IServerRoleAccessor serverRoleAccessor, - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedContentCache contentCache, IApiContentBuilder apiContentBuilder) : base( webhookFiringService, @@ -29,7 +29,7 @@ public class ContentRolledBackWebhookEvent : WebhookEventContentBase { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IApiContentBuilder _apiContentBuilder; + private readonly IPublishedContentCache _contentCache; public ContentSavedWebhookEvent( IWebhookFiringService webhookFiringService, IWebhookService webhookService, IOptionsMonitor webhookSettings, IServerRoleAccessor serverRoleAccessor, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IApiContentBuilder apiContentBuilder) + IApiContentBuilder apiContentBuilder, + IPublishedContentCache contentCache) : base( webhookFiringService, webhookService, webhookSettings, serverRoleAccessor) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; _apiContentBuilder = apiContentBuilder; + _contentCache = contentCache; } public override string Alias => Constants.WebhookEvents.Aliases.ContentSaved; @@ -40,13 +40,8 @@ public class ContentSavedWebhookEvent : WebhookEventContentBase { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IPublishedContentCache _contentCache; private readonly IApiContentBuilder _apiContentBuilder; public ContentSortedWebhookEvent( @@ -21,7 +21,7 @@ public class ContentSortedWebhookEvent : WebhookEventBase webhookSettings, IServerRoleAccessor serverRoleAccessor, - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedContentCache contentCache, IApiContentBuilder apiContentBuilder) : base( webhookFiringService, @@ -29,7 +29,7 @@ public class ContentSortedWebhookEvent : WebhookEventBase(); foreach (var entity in notification.SortedEntities) { - IPublishedContent? publishedContent = publishedSnapshot.Content.GetById(entity.Key); + IPublishedContent? publishedContent = _contentCache.GetById(entity.Key); object? payload = publishedContent is null ? null : _apiContentBuilder.Build(publishedContent); sortedEntities.Add(payload); } diff --git a/src/Umbraco.Core/Webhooks/Events/Media/MediaSavedWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Media/MediaSavedWebhookEvent.cs index c744f0c427..39c6fbe73e 100644 --- a/src/Umbraco.Core/Webhooks/Events/Media/MediaSavedWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/Events/Media/MediaSavedWebhookEvent.cs @@ -13,7 +13,7 @@ namespace Umbraco.Cms.Core.Webhooks.Events; [WebhookEvent("Media Saved", Constants.WebhookEvents.Types.Media)] public class MediaSavedWebhookEvent : WebhookEventContentBase { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IPublishedMediaCache _mediaCache; private readonly IApiMediaBuilder _apiMediaBuilder; public MediaSavedWebhookEvent( @@ -21,7 +21,7 @@ public class MediaSavedWebhookEvent : WebhookEventContentBase webhookSettings, IServerRoleAccessor serverRoleAccessor, - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedMediaCache mediaCache, IApiMediaBuilder apiMediaBuilder) : base( webhookFiringService, @@ -29,8 +29,8 @@ public class MediaSavedWebhookEvent : WebhookEventContentBase Constants.WebhookEvents.Aliases.MediaSave; @@ -39,12 +39,7 @@ public class MediaSavedWebhookEvent : WebhookEventContentBase, IApiMediaWithCropsResponseBuilder { - public ApiMediaWithCropsResponseBuilder(IApiMediaBuilder apiMediaBuilder, IPublishedValueFallback publishedValueFallback) + private readonly IPublishedMediaCache _mediaCache; + private readonly IMediaNavigationQueryService _navigationQueryService; + + public ApiMediaWithCropsResponseBuilder( + IApiMediaBuilder apiMediaBuilder, + IPublishedValueFallback publishedValueFallback, + IPublishedMediaCache mediaCache, + IMediaNavigationQueryService navigationQueryService) : base(apiMediaBuilder, publishedValueFallback) { + _mediaCache = mediaCache; + _navigationQueryService = navigationQueryService; } protected override IApiMediaWithCropsResponse Create( @@ -27,7 +38,22 @@ internal sealed class ApiMediaWithCropsResponseBuilder : ApiMediaWithCropsBuilde while (current != null) { yield return current.Name.ToLowerInvariant(); - current = current.Parent; + current = GetParent(media); } } + + private IPublishedContent? GetParent(IPublishedContent media) + { + IPublishedContent? parent; + if (_navigationQueryService.TryGetParentKey(media.Key, out Guid? parentKey)) + { + parent = parentKey.HasValue ? _mediaCache.GetById(parentKey.Value) : null; + } + else + { + throw new KeyNotFoundException($"Media with key '{media.Key}' was not found in the in-memory navigation structure."); + } + + return parent; + } } diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs index ec2d710087..527d1e1547 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs @@ -1,13 +1,10 @@ using HtmlAgilityPack; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Infrastructure.Extensions; using Umbraco.Extensions; @@ -15,7 +12,8 @@ namespace Umbraco.Cms.Infrastructure.DeliveryApi; internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRichTextElementParser { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IPublishedContentCache _publishedContentCache; + private readonly IPublishedMediaCache _publishedMediaCache; private readonly IApiElementBuilder _apiElementBuilder; private readonly ILogger _logger; @@ -25,12 +23,14 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich public ApiRichTextElementParser( IApiContentRouteBuilder apiContentRouteBuilder, IApiMediaUrlProvider mediaUrlProvider, - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedContentCache publishedContentCache, + IPublishedMediaCache publishedMediaCache, IApiElementBuilder apiElementBuilder, ILogger logger) : base(apiContentRouteBuilder, mediaUrlProvider) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; + _publishedContentCache = publishedContentCache; + _publishedMediaCache = publishedMediaCache; _apiElementBuilder = apiElementBuilder; _logger = logger; } @@ -42,10 +42,9 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich { try { - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); var doc = new HtmlDocument(); doc.LoadHtml(html); - return ParseRootElement(doc.DocumentNode, publishedSnapshot, richTextBlockModel); + return ParseRootElement(doc.DocumentNode, _publishedContentCache, _publishedMediaCache, richTextBlockModel); } catch (Exception ex) { @@ -54,10 +53,10 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich } } - private IRichTextElement ParseRecursively(HtmlNode current, IPublishedSnapshot publishedSnapshot) + private IRichTextElement ParseRecursively(HtmlNode current, IPublishedContentCache contentCache, IPublishedMediaCache mediaCache) => current.Name == TextNodeName ? ParseTextElement(current) - : ParseGenericElement(current, publishedSnapshot); + : ParseGenericElement(current, contentCache, mediaCache); private RichTextTextElement ParseTextElement(HtmlNode element) { @@ -69,7 +68,7 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich return new RichTextTextElement(element.InnerText); } - private RichTextRootElement ParseRootElement(HtmlNode element, IPublishedSnapshot publishedSnapshot, RichTextBlockModel? richTextBlockModel) + private RichTextRootElement ParseRootElement(HtmlNode element, IPublishedContentCache contentCache, IPublishedMediaCache mediaCache, RichTextBlockModel? richTextBlockModel) { ApiBlockItem[] blocks = richTextBlockModel is not null ? richTextBlockModel.Select(item => item.CreateApiBlockItem(_apiElementBuilder)).ToArray() @@ -77,11 +76,12 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich return ParseElement( element, - publishedSnapshot, + contentCache, + mediaCache, (_, attributes, childElements) => new RichTextRootElement(attributes, childElements, blocks)); } - private RichTextGenericElement ParseGenericElement(HtmlNode element, IPublishedSnapshot publishedSnapshot) + private RichTextGenericElement ParseGenericElement(HtmlNode element, IPublishedContentCache contentCache, IPublishedMediaCache mediaCache) { if (element.Name == TextNodeName) { @@ -90,11 +90,12 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich return ParseElement( element, - publishedSnapshot, + contentCache, + mediaCache, (tag, attributes, childElements) => new RichTextGenericElement(tag, attributes, childElements)); } - private T ParseElement(HtmlNode element, IPublishedSnapshot publishedSnapshot, Func, IRichTextElement[], T> createElement) + private T ParseElement(HtmlNode element, IPublishedContentCache contentCache, IPublishedMediaCache mediaCache, Func, IRichTextElement[], T> createElement) where T : IRichTextElement { // grab all valid node children: @@ -108,16 +109,16 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich var tag = TagName(element); var attributes = element.Attributes.ToDictionary(a => a.Name, a => a.Value as object); - ReplaceLocalLinks(publishedSnapshot, attributes); + ReplaceLocalLinks(contentCache, mediaCache, attributes); - ReplaceLocalImages(publishedSnapshot, tag, attributes); + ReplaceLocalImages(mediaCache, tag, attributes); CleanUpBlocks(tag, attributes); SanitizeAttributes(attributes); IRichTextElement[] childElements = childNodes.Any() - ? childNodes.Select(child => ParseRecursively(child, publishedSnapshot)).ToArray() + ? childNodes.Select(child => ParseRecursively(child, contentCache, mediaCache)).ToArray() : Array.Empty(); return createElement(tag, attributes, childElements); @@ -125,7 +126,7 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich private string TagName(HtmlNode htmlNode) => htmlNode.Name; - private void ReplaceLocalLinks(IPublishedSnapshot publishedSnapshot, Dictionary attributes) + private void ReplaceLocalLinks(IPublishedContentCache contentCache, IPublishedMediaCache mediaCache, Dictionary attributes) { if (attributes.ContainsKey("href") is false || attributes["href"] is not string href) { @@ -138,7 +139,8 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich } ReplaceLocalLinks( - publishedSnapshot, + contentCache, + mediaCache, href, type, route => @@ -150,14 +152,14 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich () => attributes.Remove("href")); } - private void ReplaceLocalImages(IPublishedSnapshot publishedSnapshot, string tag, Dictionary attributes) + private void ReplaceLocalImages(IPublishedMediaCache mediaCache, string tag, Dictionary attributes) { if (tag is not "img" || attributes.ContainsKey("data-udi") is false || attributes["data-udi"] is not string dataUdi) { return; } - ReplaceLocalImages(publishedSnapshot, dataUdi, mediaUrl => + ReplaceLocalImages(mediaCache, dataUdi, mediaUrl => { attributes["src"] = mediaUrl; attributes.Remove("data-udi"); diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs index fcb55258d0..42418146db 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs @@ -9,17 +9,20 @@ namespace Umbraco.Cms.Infrastructure.DeliveryApi; internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichTextMarkupParser { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IPublishedContentCache _publishedContentCache; + private readonly IPublishedMediaCache _publishedMediaCache; private readonly ILogger _logger; public ApiRichTextMarkupParser( IApiContentRouteBuilder apiContentRouteBuilder, IApiMediaUrlProvider mediaUrlProvider, - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedContentCache publishedContentCache, + IPublishedMediaCache publishedMediaCache, ILogger logger) : base(apiContentRouteBuilder, mediaUrlProvider) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; + _publishedContentCache = publishedContentCache; + _publishedMediaCache = publishedMediaCache; _logger = logger; } @@ -27,13 +30,12 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT { try { - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); var doc = new HtmlDocument(); doc.LoadHtml(html); - ReplaceLocalLinks(doc, publishedSnapshot); + ReplaceLocalLinks(doc, _publishedContentCache, _publishedMediaCache); - ReplaceLocalImages(doc, publishedSnapshot); + ReplaceLocalImages(doc, _publishedMediaCache); CleanUpBlocks(doc); @@ -46,13 +48,14 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT } } - private void ReplaceLocalLinks(HtmlDocument doc, IPublishedSnapshot publishedSnapshot) + private void ReplaceLocalLinks(HtmlDocument doc, IPublishedContentCache contentCache, IPublishedMediaCache mediaCache) { HtmlNode[] links = doc.DocumentNode.SelectNodes("//a")?.ToArray() ?? Array.Empty(); foreach (HtmlNode link in links) { ReplaceLocalLinks( - publishedSnapshot, + contentCache, + mediaCache, link.GetAttributeValue("href", string.Empty), link.GetAttributeValue("type", "unknown"), route => @@ -75,7 +78,7 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT } } - private void ReplaceLocalImages(HtmlDocument doc, IPublishedSnapshot publishedSnapshot) + private void ReplaceLocalImages(HtmlDocument doc, IPublishedMediaCache mediaCache) { HtmlNode[] images = doc.DocumentNode.SelectNodes("//img")?.ToArray() ?? Array.Empty(); foreach (HtmlNode image in images) @@ -86,7 +89,7 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT continue; } - ReplaceLocalImages(publishedSnapshot, dataUdi, mediaUrl => + ReplaceLocalImages(mediaCache, dataUdi, mediaUrl => { // the image source likely contains query string parameters for image cropping; we need to // preserve those, so let's extract the image query string (if present). diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs index dd88453fab..520d5e8cb7 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs @@ -21,21 +21,21 @@ internal abstract partial class ApiRichTextParserBase _apiMediaUrlProvider = apiMediaUrlProvider; } - protected void ReplaceLocalLinks(IPublishedSnapshot publishedSnapshot, string href, string type, Action handleContentRoute, Action handleMediaUrl, Action handleInvalidLink) + protected void ReplaceLocalLinks(IPublishedContentCache contentCache, IPublishedMediaCache mediaCache, string href, string type, Action handleContentRoute, Action handleMediaUrl, Action handleInvalidLink) { - ReplaceStatus replaceAttempt = ReplaceLocalLink(publishedSnapshot, href, type, handleContentRoute, handleMediaUrl); + ReplaceStatus replaceAttempt = ReplaceLocalLink(contentCache, mediaCache, href, type, handleContentRoute, handleMediaUrl); if (replaceAttempt == ReplaceStatus.Success) { return; } - if (replaceAttempt == ReplaceStatus.InvalidEntityType || ReplaceLegacyLocalLink(publishedSnapshot, href, handleContentRoute, handleMediaUrl) == ReplaceStatus.InvalidEntityType) + if (replaceAttempt == ReplaceStatus.InvalidEntityType || ReplaceLegacyLocalLink(contentCache, mediaCache, href, handleContentRoute, handleMediaUrl) == ReplaceStatus.InvalidEntityType) { handleInvalidLink(); } } - private ReplaceStatus ReplaceLocalLink(IPublishedSnapshot publishedSnapshot, string href, string type, Action handleContentRoute, Action handleMediaUrl) + private ReplaceStatus ReplaceLocalLink(IPublishedContentCache contentCache, IPublishedMediaCache mediaCache, string href, string type, Action handleContentRoute, Action handleMediaUrl) { Match match = LocalLinkRegex().Match(href); if (match.Success is false) @@ -53,7 +53,7 @@ internal abstract partial class ApiRichTextParserBase switch (udi.EntityType) { case Constants.UdiEntityType.Document: - IPublishedContent? content = publishedSnapshot.Content?.GetById(udi); + IPublishedContent? content = contentCache.GetById(guid); IApiContentRoute? route = content != null ? _apiContentRouteBuilder.Build(content) : null; @@ -65,7 +65,7 @@ internal abstract partial class ApiRichTextParserBase break; case Constants.UdiEntityType.Media: - IPublishedContent? media = publishedSnapshot.Media?.GetById(udi); + IPublishedContent? media = mediaCache.GetById(guid); if (media != null) { handleMediaUrl(_apiMediaUrlProvider.GetUrl(media)); @@ -78,7 +78,7 @@ internal abstract partial class ApiRichTextParserBase return ReplaceStatus.InvalidEntityType; } - private ReplaceStatus ReplaceLegacyLocalLink(IPublishedSnapshot publishedSnapshot, string href, Action handleContentRoute, Action handleMediaUrl) + private ReplaceStatus ReplaceLegacyLocalLink(IPublishedContentCache contentCache, IPublishedMediaCache mediaCache, string href, Action handleContentRoute, Action handleMediaUrl) { Match match = LegacyLocalLinkRegex().Match(href); if (match.Success is false) @@ -91,11 +91,16 @@ internal abstract partial class ApiRichTextParserBase return ReplaceStatus.NoMatch; } + // Looking at the old NuCache implementation, Udi's HAD to be GuidUdi's, so we'll assume that here too + if(udi is not GuidUdi guidUdi) + { + return ReplaceStatus.NoMatch; + } switch (udi.EntityType) { case Constants.UdiEntityType.Document: - IPublishedContent? content = publishedSnapshot.Content?.GetById(udi); + IPublishedContent? content = contentCache.GetById(guidUdi.Guid); IApiContentRoute? route = content != null ? _apiContentRouteBuilder.Build(content) : null; @@ -107,7 +112,7 @@ internal abstract partial class ApiRichTextParserBase break; case Constants.UdiEntityType.Media: - IPublishedContent? media = publishedSnapshot.Media?.GetById(udi); + IPublishedContent? media = mediaCache.GetById(guidUdi.Guid); if (media != null) { handleMediaUrl(_apiMediaUrlProvider.GetUrl(media)); @@ -120,14 +125,14 @@ internal abstract partial class ApiRichTextParserBase return ReplaceStatus.InvalidEntityType; } - protected void ReplaceLocalImages(IPublishedSnapshot publishedSnapshot, string udi, Action handleMediaUrl) + protected void ReplaceLocalImages(IPublishedMediaCache mediaCache, string udi, Action handleMediaUrl) { - if (UdiParser.TryParse(udi, out Udi? udiValue) is false) + if (UdiParser.TryParse(udi, out Udi? udiValue) is false || udiValue is not GuidUdi guidUdi) { return; } - IPublishedContent? media = publishedSnapshot.Media?.GetById(udiValue); + IPublishedContent? media = mediaCache.GetById(guidUdi.Guid); if (media is null) { return; diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index cdb00380b9..a7a71d3300 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -146,10 +146,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(factory => new MigrationBuilder(factory)); - builder.Services.AddSingleton(); - - // register the published snapshot accessor - the "current" published snapshot is in the umbraco context - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -196,10 +193,11 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddScoped(factory => { IUmbracoContextAccessor umbCtx = factory.GetRequiredService(); - IUmbracoContext umbracoContext = umbCtx.GetRequiredUmbracoContext(); return new PublishedContentQuery( - umbracoContext.PublishedSnapshot, - factory.GetRequiredService(), factory.GetRequiredService()); + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService()); }); // register accessors for cultures diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 5bd401be67..042bf82e45 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -18,6 +19,7 @@ using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Packaging; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Services; using Umbraco.Cms.Infrastructure.Services.Implement; @@ -77,6 +79,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.TryAddTransient(); return builder; } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs b/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs index 6ac45b4184..afefb4e8e8 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs @@ -55,7 +55,7 @@ public static class ExamineExtensions /// . /// /// The search results. - /// The snapshot. + /// The caches. /// /// An containing all content, media or members. /// @@ -65,11 +65,11 @@ public static class ExamineExtensions /// public static IEnumerable ToPublishedSearchResults( this IEnumerable results, - IPublishedSnapshot snapshot) + ICacheManager cacheManager) { - if (snapshot == null) + if (cacheManager == null) { - throw new ArgumentNullException(nameof(snapshot)); + throw new ArgumentNullException(nameof(cacheManager)); } var publishedSearchResults = new List(); @@ -83,10 +83,10 @@ public static class ExamineExtensions switch (indexType) { case IndexTypes.Content: - content = snapshot.Content?.GetById(contentId); + content = cacheManager.Content?.GetById(contentId); break; case IndexTypes.Media: - content = snapshot.Media?.GetById(contentId); + content = cacheManager.Media?.GetById(contentId); break; case IndexTypes.Member: throw new NotSupportedException("Cannot convert search results to member instances"); diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs index bf2d734305..06d85abe9f 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs @@ -44,7 +44,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor private readonly ILoggerFactory _loggerFactory; private readonly IMigrationBuilder _migrationBuilder; private readonly IUmbracoDatabaseFactory _databaseFactory; - private readonly IPublishedSnapshotService _publishedSnapshotService; + private readonly IDatabaseCacheRebuilder _databaseCacheRebuilder; private readonly IKeyValueService _keyValueService; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly DistributedCache _distributedCache; @@ -59,7 +59,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor ILoggerFactory loggerFactory, IMigrationBuilder migrationBuilder, IUmbracoDatabaseFactory databaseFactory, - IPublishedSnapshotService publishedSnapshotService, + IDatabaseCacheRebuilder databaseCacheRebuilder, DistributedCache distributedCache, IKeyValueService keyValueService, IServiceScopeFactory serviceScopeFactory) @@ -69,54 +69,13 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor _loggerFactory = loggerFactory; _migrationBuilder = migrationBuilder; _databaseFactory = databaseFactory; - _publishedSnapshotService = publishedSnapshotService; + _databaseCacheRebuilder = databaseCacheRebuilder; _keyValueService = keyValueService; _serviceScopeFactory = serviceScopeFactory; _distributedCache = distributedCache; _logger = _loggerFactory.CreateLogger(); } - [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")] - public MigrationPlanExecutor( - ICoreScopeProvider scopeProvider, - IScopeAccessor scopeAccessor, - ILoggerFactory loggerFactory, - IMigrationBuilder migrationBuilder, - IUmbracoDatabaseFactory databaseFactory, - IPublishedSnapshotService publishedSnapshotService, - DistributedCache distributedCache) - : this( - scopeProvider, - scopeAccessor, - loggerFactory, - migrationBuilder, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) - { - } - - [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")] - public MigrationPlanExecutor( - ICoreScopeProvider scopeProvider, - IScopeAccessor scopeAccessor, - ILoggerFactory loggerFactory, - IMigrationBuilder migrationBuilder) - : this( - scopeProvider, - scopeAccessor, - loggerFactory, - migrationBuilder, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) - { - } - public string Execute(MigrationPlan plan, string fromState) => ExecutePlan(plan, fromState).FinalState; /// @@ -344,7 +303,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor private void RebuildCache() { - _publishedSnapshotService.RebuildAll(); + _databaseCacheRebuilder.Rebuild(); _distributedCache.RefreshAllPublishedSnapshot(); } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/CacheRebuilder.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/CacheRebuilder.cs new file mode 100644 index 0000000000..29ba8b3878 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/CacheRebuilder.cs @@ -0,0 +1,32 @@ +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Implements in Umbraco.Web (rebuilding). +/// +public class CacheRebuilder : ICacheRebuilder +{ + private readonly DistributedCache _distributedCache; + private readonly IDatabaseCacheRebuilder _databaseCacheRebuilder; + + /// + /// Initializes a new instance of the class. + /// + public CacheRebuilder( + DistributedCache distributedCache, + IDatabaseCacheRebuilder databaseCacheRebuilder) + { + _distributedCache = distributedCache; + _databaseCacheRebuilder = databaseCacheRebuilder; + } + + /// + public void Rebuild() + { + _databaseCacheRebuilder.Rebuild(); + _distributedCache.RefreshAllPublishedSnapshot(); + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ICacheRebuilder.cs similarity index 91% rename from src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs rename to src/Umbraco.Infrastructure/Migrations/PostMigrations/ICacheRebuilder.cs index 35e1fb7a30..ee2e72ee52 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ICacheRebuilder.cs @@ -10,7 +10,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; /// be refactored, really. /// /// -public interface IPublishedSnapshotRebuilder +public interface ICacheRebuilder { /// /// Rebuilds. diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs deleted file mode 100644 index d86307b1f9..0000000000 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; - -/// -/// Implements in Umbraco.Web (rebuilding). -/// -public class PublishedSnapshotRebuilder : IPublishedSnapshotRebuilder -{ - private readonly DistributedCache _distributedCache; - private readonly IPublishedSnapshotService _publishedSnapshotService; - - /// - /// Initializes a new instance of the class. - /// - public PublishedSnapshotRebuilder( - IPublishedSnapshotService publishedSnapshotService, - DistributedCache distributedCache) - { - _publishedSnapshotService = publishedSnapshotService; - _distributedCache = distributedCache; - } - - /// - public void Rebuild() - { - _publishedSnapshotService.RebuildAll(); - _distributedCache.RefreshAllPublishedSnapshot(); - } -} diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs index 5054e610db..a2d24badbe 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs @@ -286,16 +286,16 @@ public class TextBuilder : Builder WriteGeneratedCodeAttribute(sb, "\t\t"); WriteMaybeNullAttribute(sb, "\t\t", true); sb.Append( - "\t\tpublic new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor)\n"); + "\t\tpublic new static IPublishedContentType GetModelContentType(IPublishedContentTypeCache contentTypeCache)\n"); sb.Append( - "\t\t\t=> PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias);\n"); + "\t\t\t=> PublishedModelUtility.GetModelContentType(contentTypeCache, ModelItemType, ModelTypeAlias);\n"); WriteGeneratedCodeAttribute(sb, "\t\t"); WriteMaybeNullAttribute(sb, "\t\t", true); sb.AppendFormat( - "\t\tpublic static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector)\n", + "\t\tpublic static IPublishedPropertyType GetModelPropertyType(IPublishedContentTypeCache contentTypeCache, Expression> selector)\n", type.ClrName); sb.Append( - "\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector);\n"); + "\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(contentTypeCache), selector);\n"); sb.Append("#pragma warning restore 0109\n\n"); sb.Append("\t\tprivate IPublishedValueFallback _publishedValueFallback;"); diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs index dd99ddaf28..2436eaca1f 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs @@ -29,20 +29,20 @@ public static class PublishedModelUtility // // etc... // } public static IPublishedContentType? GetModelContentType( - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedContentTypeCache contentTypeCache, PublishedItemType itemType, string alias) { - IPublishedSnapshot publishedSnapshot = publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); switch (itemType) { case PublishedItemType.Content: + return contentTypeCache.Get(PublishedItemType.Content, alias); case PublishedItemType.Element: - return publishedSnapshot.Content?.GetContentType(alias); + return contentTypeCache.Get(PublishedItemType.Element, alias); case PublishedItemType.Media: - return publishedSnapshot.Media?.GetContentType(alias); + return contentTypeCache.Get(PublishedItemType.Media, alias); case PublishedItemType.Member: - return publishedSnapshot.Members?.GetContentType(alias); + return contentTypeCache.Get(PublishedItemType.Member, alias); default: throw new ArgumentOutOfRangeException(nameof(itemType)); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs index 9c7416602b..5097c99fa4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs @@ -256,6 +256,7 @@ internal class LanguageRepository : EntityRepositoryBase, ILangu "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE tagId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentUrl + " WHERE languageId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Language + " WHERE id = @id", }; return list; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs index 2dc70eea0d..792cc346dd 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs @@ -13,18 +13,21 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; /// public sealed class BlockEditorConverter { + private readonly IPublishedContentTypeCache _publishedContentTypeCache; + private readonly ICacheManager _cacheManager; private readonly IPublishedModelFactory _publishedModelFactory; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IVariationContextAccessor _variationContextAccessor; private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler; public BlockEditorConverter( - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedContentTypeCache publishedContentTypeCache, + ICacheManager cacheManager, IPublishedModelFactory publishedModelFactory, IVariationContextAccessor variationContextAccessor, BlockEditorVarianceHandler blockEditorVarianceHandler) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; + _publishedContentTypeCache = publishedContentTypeCache; + _cacheManager = cacheManager; _publishedModelFactory = publishedModelFactory; _variationContextAccessor = variationContextAccessor; _blockEditorVarianceHandler = blockEditorVarianceHandler; @@ -32,11 +35,8 @@ public sealed class BlockEditorConverter public IPublishedElement? ConvertToElement(IPublishedElement owner, BlockItemData data, PropertyCacheLevel referenceCacheLevel, bool preview) { - IPublishedContentCache? publishedContentCache = - _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Content; - // Only convert element types - content types will cause an exception when PublishedModelFactory creates the model - IPublishedContentType? publishedContentType = publishedContentCache?.GetContentType(data.ContentTypeKey); + IPublishedContentType? publishedContentType = _publishedContentTypeCache.Get(PublishedItemType.Element, data.ContentTypeKey); if (publishedContentType == null || publishedContentType.IsElement == false) { return null; @@ -91,7 +91,7 @@ public sealed class BlockEditorConverter return null; } - IPublishedElement element = new PublishedElement(publishedContentType, key, propertyValues, preview, referenceCacheLevel, _publishedSnapshotAccessor); + IPublishedElement element = new PublishedElement(publishedContentType, key, propertyValues, preview, referenceCacheLevel, _cacheManager); element = _publishedModelFactory.CreateModel(element); return element; @@ -99,9 +99,7 @@ public sealed class BlockEditorConverter public Type GetModelType(Guid contentTypeKey) { - IPublishedContentCache? publishedContentCache = - _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Content; - IPublishedContentType? publishedContentType = publishedContentCache?.GetContentType(contentTypeKey); + IPublishedContentType? publishedContentType = _publishedContentTypeCache.Get(PublishedItemType.Content, contentTypeKey); if (publishedContentType is not null && publishedContentType.IsElement) { return _publishedModelFactory.GetModelType(publishedContentType.Alias); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs index c59b61de82..187cf372ca 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs @@ -14,20 +14,19 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase, IDeliveryApiPropertyValueConverter { private readonly IJsonSerializer _jsonSerializer; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IPublishedMediaCache _publishedMediaCache; private readonly IPublishedUrlProvider _publishedUrlProvider; private readonly IPublishedValueFallback _publishedValueFallback; private readonly IApiMediaWithCropsBuilder _apiMediaWithCropsBuilder; public MediaPickerWithCropsValueConverter( - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedMediaCache publishedMediaCache, IPublishedUrlProvider publishedUrlProvider, IPublishedValueFallback publishedValueFallback, IJsonSerializer jsonSerializer, IApiMediaWithCropsBuilder apiMediaWithCropsBuilder) { - _publishedSnapshotAccessor = publishedSnapshotAccessor ?? - throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); + _publishedMediaCache = publishedMediaCache; _publishedUrlProvider = publishedUrlProvider; _publishedValueFallback = publishedValueFallback; _jsonSerializer = jsonSerializer; @@ -70,10 +69,9 @@ public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase, ID IEnumerable dtos = MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor.Deserialize(_jsonSerializer, inter); MediaPicker3Configuration? configuration = propertyType.DataType.ConfigurationAs(); - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); foreach (MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor.MediaWithCropsDto dto in dtos) { - IPublishedContent? mediaItem = publishedSnapshot.Media?.GetById(preview, dto.MediaKey); + IPublishedContent? mediaItem = _publishedMediaCache.GetById(preview, dto.MediaKey); if (mediaItem != null) { var localCrops = new ImageCropperValue diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs index 68dfe6a6ac..a520fa35ef 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs @@ -2,7 +2,6 @@ // See LICENSE for more details. using Umbraco.Cms.Core.DeliveryApi; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.DeliveryApi; @@ -11,7 +10,6 @@ using Umbraco.Cms.Core.PropertyEditors.DeliveryApi; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Web; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; @@ -21,30 +19,31 @@ public class MultiUrlPickerValueConverter : PropertyValueConverterBase, IDeliver { private readonly IJsonSerializer _jsonSerializer; private readonly IProfilingLogger _proflog; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IPublishedUrlProvider _publishedUrlProvider; private readonly IApiContentNameProvider _apiContentNameProvider; private readonly IApiMediaUrlProvider _apiMediaUrlProvider; private readonly IApiContentRouteBuilder _apiContentRouteBuilder; + private readonly IPublishedContentCache _contentCache; + private readonly IPublishedMediaCache _mediaCache; public MultiUrlPickerValueConverter( - IPublishedSnapshotAccessor publishedSnapshotAccessor, IProfilingLogger proflog, IJsonSerializer jsonSerializer, - IUmbracoContextAccessor umbracoContextAccessor, IPublishedUrlProvider publishedUrlProvider, IApiContentNameProvider apiContentNameProvider, IApiMediaUrlProvider apiMediaUrlProvider, - IApiContentRouteBuilder apiContentRouteBuilder) + IApiContentRouteBuilder apiContentRouteBuilder, + IPublishedContentCache contentCache, + IPublishedMediaCache mediaCache) { - _publishedSnapshotAccessor = publishedSnapshotAccessor ?? - throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); _proflog = proflog ?? throw new ArgumentNullException(nameof(proflog)); _jsonSerializer = jsonSerializer; _publishedUrlProvider = publishedUrlProvider; _apiContentNameProvider = apiContentNameProvider; _apiMediaUrlProvider = apiMediaUrlProvider; _apiContentRouteBuilder = apiContentRouteBuilder; + _contentCache = contentCache; + _mediaCache = mediaCache; } public override bool IsConverter(IPublishedPropertyType propertyType) => @@ -77,7 +76,6 @@ public class MultiUrlPickerValueConverter : PropertyValueConverterBase, IDeliver var links = new List(); IEnumerable? dtos = ParseLinkDtos(inter.ToString()!); - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); if (dtos is null) { return links; @@ -95,8 +93,8 @@ public class MultiUrlPickerValueConverter : PropertyValueConverterBase, IDeliver : LinkType.Content; IPublishedContent? content = type == LinkType.Media - ? publishedSnapshot.Media?.GetById(preview, dto.Udi.Guid) - : publishedSnapshot.Content?.GetById(preview, dto.Udi.Guid); + ? _mediaCache.GetById(preview, dto.Udi.Guid) + : _contentCache.GetById(preview, dto.Udi.Guid); if (content == null || content.ContentType.ItemType == PublishedItemType.Element) { @@ -150,14 +148,12 @@ public class MultiUrlPickerValueConverter : PropertyValueConverterBase, IDeliver return DefaultValue(); } - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - ApiLink? ToLink(MultiUrlPickerValueEditor.LinkDto item) { switch (item.Udi?.EntityType) { case Constants.UdiEntityType.Document: - IPublishedContent? content = publishedSnapshot.Content?.GetById(item.Udi.Guid); + IPublishedContent? content = _contentCache.GetById(item.Udi.Guid); IApiContentRoute? route = content != null ? _apiContentRouteBuilder.Build(content) : null; @@ -171,7 +167,7 @@ public class MultiUrlPickerValueConverter : PropertyValueConverterBase, IDeliver content.ContentType.Alias, route); case Constants.UdiEntityType.Media: - IPublishedContent? media = publishedSnapshot.Media?.GetById(item.Udi.Guid); + IPublishedContent? media = _mediaCache.GetById(item.Udi.Guid); return media == null ? null : ApiLink.Media( diff --git a/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs b/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs index 74f27ba8dd..204f7a910c 100644 --- a/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs +++ b/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs @@ -302,6 +302,9 @@ public class PublishedContentTypeCache : IPublishedContentTypeCache case PublishedItemType.Member: k = "m"; break; + case PublishedItemType.Element: + k = "e"; + break; default: throw new ArgumentOutOfRangeException(nameof(itemType)); } @@ -314,6 +317,7 @@ public class PublishedContentTypeCache : IPublishedContentTypeCache IContentTypeComposition? contentType = itemType switch { PublishedItemType.Content => _contentTypeService?.Get(key), + PublishedItemType.Element => _contentTypeService?.Get(key), PublishedItemType.Media => _mediaTypeService?.Get(key), PublishedItemType.Member => _memberTypeService?.Get(key), _ => throw new ArgumentOutOfRangeException(nameof(itemType)), diff --git a/src/Umbraco.PublishedCache.NuCache/ReservedFieldNamesService.cs b/src/Umbraco.Infrastructure/PublishedCache/ReservedFieldNamesService.cs similarity index 90% rename from src/Umbraco.PublishedCache.NuCache/ReservedFieldNamesService.cs rename to src/Umbraco.Infrastructure/PublishedCache/ReservedFieldNamesService.cs index 5243a6a8d6..8cbdf4f130 100644 --- a/src/Umbraco.PublishedCache.NuCache/ReservedFieldNamesService.cs +++ b/src/Umbraco.Infrastructure/PublishedCache/ReservedFieldNamesService.cs @@ -44,8 +44,8 @@ internal class ReservedFieldNamesService : IReservedFieldNamesService public ISet GetMemberReservedFieldNames() { - var reservedProperties = typeof(PublishedMember).GetPublicProperties().Select(x => x.Name).ToHashSet(); - var reservedMethods = typeof(PublishedMember).GetPublicMethods().Select(x => x.Name).ToHashSet(); + var reservedProperties = typeof(IPublishedMember).GetPublicProperties().Select(x => x.Name).ToHashSet(); + var reservedMethods = typeof(IPublishedMember).GetPublicMethods().Select(x => x.Name).ToHashSet(); reservedProperties.UnionWith(reservedMethods); reservedProperties.UnionWith(_memberPropertySettings.ReservedFieldNames); diff --git a/src/Umbraco.Infrastructure/PublishedContentQuery.cs b/src/Umbraco.Infrastructure/PublishedContentQuery.cs index e0cc0a97bd..288a93165f 100644 --- a/src/Umbraco.Infrastructure/PublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/PublishedContentQuery.cs @@ -17,7 +17,8 @@ namespace Umbraco.Cms.Infrastructure; public class PublishedContentQuery : IPublishedContentQuery { private readonly IExamineManager _examineManager; - private readonly IPublishedSnapshot _publishedSnapshot; + private readonly IPublishedContentCache _publishedContent; + private readonly IPublishedMediaCache _publishedMediaCache; private readonly IVariationContextAccessor _variationContextAccessor; private static readonly HashSet _returnedQueryFields = new() { ExamineFieldNames.ItemIdFieldName, ExamineFieldNames.CategoryFieldName }; @@ -25,13 +26,17 @@ public class PublishedContentQuery : IPublishedContentQuery /// /// Initializes a new instance of the class. /// - public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, - IVariationContextAccessor variationContextAccessor, IExamineManager examineManager) + public PublishedContentQuery( + IVariationContextAccessor variationContextAccessor, + IExamineManager examineManager, + IPublishedContentCache publishedContent, + IPublishedMediaCache publishedMediaCache) { - _publishedSnapshot = publishedSnapshot ?? throw new ArgumentNullException(nameof(publishedSnapshot)); _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); + _publishedContent = publishedContent; + _publishedMediaCache = publishedMediaCache; } #region Convert Helpers @@ -92,10 +97,10 @@ public class PublishedContentQuery : IPublishedContentQuery #region Content public IPublishedContent? Content(int id) - => ItemById(id, _publishedSnapshot.Content); + => ItemById(id, _publishedContent); public IPublishedContent? Content(Guid id) - => ItemById(id, _publishedSnapshot.Content); + => ItemById(id, _publishedContent); public IPublishedContent? Content(Udi? id) { @@ -104,7 +109,7 @@ public class PublishedContentQuery : IPublishedContentQuery return null; } - return ItemById(udi.Guid, _publishedSnapshot.Content); + return ItemById(udi.Guid, _publishedContent); } public IPublishedContent? Content(object id) @@ -128,26 +133,26 @@ public class PublishedContentQuery : IPublishedContentQuery } public IEnumerable Content(IEnumerable ids) - => ItemsByIds(_publishedSnapshot.Content, ids); + => ItemsByIds(_publishedContent, ids); public IEnumerable Content(IEnumerable ids) - => ItemsByIds(_publishedSnapshot.Content, ids); + => ItemsByIds(_publishedContent, ids); public IEnumerable Content(IEnumerable ids) => ids.Select(Content).WhereNotNull(); public IEnumerable ContentAtRoot() - => ItemsAtRoot(_publishedSnapshot.Content); + => ItemsAtRoot(_publishedContent); #endregion #region Media public IPublishedContent? Media(int id) - => ItemById(id, _publishedSnapshot.Media); + => ItemById(id, _publishedMediaCache); public IPublishedContent? Media(Guid id) - => ItemById(id, _publishedSnapshot.Media); + => ItemById(id, _publishedMediaCache); public IPublishedContent? Media(Udi? id) { @@ -156,7 +161,7 @@ public class PublishedContentQuery : IPublishedContentQuery return null; } - return ItemById(udi.Guid, _publishedSnapshot.Media); + return ItemById(udi.Guid, _publishedMediaCache); } public IPublishedContent? Media(object id) @@ -180,16 +185,16 @@ public class PublishedContentQuery : IPublishedContentQuery } public IEnumerable Media(IEnumerable ids) - => ItemsByIds(_publishedSnapshot.Media, ids); + => ItemsByIds(_publishedMediaCache, ids); public IEnumerable Media(IEnumerable ids) => ids.Select(Media).WhereNotNull(); public IEnumerable Media(IEnumerable ids) - => ItemsByIds(_publishedSnapshot.Media, ids); + => ItemsByIds(_publishedMediaCache, ids); public IEnumerable MediaAtRoot() - => ItemsAtRoot(_publishedSnapshot.Media); + => ItemsAtRoot(_publishedMediaCache); #endregion @@ -317,8 +322,8 @@ public class PublishedContentQuery : IPublishedContentQuery totalRecords = results.TotalItemCount; return culture.IsNullOrWhiteSpace() - ? results.ToPublishedSearchResults(_publishedSnapshot) - : new CultureContextualSearchResults(results.ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); + ? results.ToPublishedSearchResults(_publishedContent) + : new CultureContextualSearchResults(results.ToPublishedSearchResults(_publishedContent), _variationContextAccessor, culture); } /// diff --git a/src/Umbraco.Infrastructure/Routing/ContentFinderByConfigured404.cs b/src/Umbraco.Infrastructure/Routing/ContentFinderByConfigured404.cs index 32dd1219b2..d518ff1b1b 100644 --- a/src/Umbraco.Infrastructure/Routing/ContentFinderByConfigured404.cs +++ b/src/Umbraco.Infrastructure/Routing/ContentFinderByConfigured404.cs @@ -89,7 +89,7 @@ public class ContentFinderByConfigured404 : IContentLastChanceFinder if (node != null) { Domain? d = DomainUtilities.FindWildcardDomainInPath( - umbracoContext.PublishedSnapshot.Domains?.GetAll(true), node.Path, null); + umbracoContext.Domains?.GetAll(true), node.Path, null); if (d != null) { errorCulture = d.Culture; @@ -100,7 +100,7 @@ public class ContentFinderByConfigured404 : IContentLastChanceFinder var error404 = NotFoundHandlerHelper.GetCurrentNotFoundPageId( _contentSettings.Error404Collection.ToArray(), _entityService, - new PublishedContentQuery(umbracoContext.PublishedSnapshot, _variationContextAccessor, _examineManager), + new PublishedContentQuery(_variationContextAccessor, _examineManager, umbracoContext.Content!, umbracoContext.Media), errorCulture, domainContentId); diff --git a/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs b/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs index a1247313d2..067fa91fa4 100644 --- a/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs +++ b/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -17,6 +18,8 @@ namespace Umbraco.Cms.Infrastructure.Routing private readonly IVariationContextAccessor _variationContextAccessor; private readonly ILocalizationService _localizationService; private readonly IRedirectUrlService _redirectUrlService; + private readonly IPublishedContentCache _contentCache; + private readonly IDocumentNavigationQueryService _navigationQueryService; private readonly ILogger _logger; public RedirectTracker( @@ -24,33 +27,35 @@ namespace Umbraco.Cms.Infrastructure.Routing IVariationContextAccessor variationContextAccessor, ILocalizationService localizationService, IRedirectUrlService redirectUrlService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService, ILogger logger) { _umbracoContextFactory = umbracoContextFactory; _variationContextAccessor = variationContextAccessor; _localizationService = localizationService; _redirectUrlService = redirectUrlService; + _contentCache = contentCache; + _navigationQueryService = navigationQueryService; _logger = logger; } /// public void StoreOldRoute(IContent entity, Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes) { - using UmbracoContextReference reference = _umbracoContextFactory.EnsureUmbracoContext(); - IPublishedContentCache? contentCache = reference.UmbracoContext.Content; - IPublishedContent? entityContent = contentCache?.GetById(entity.Id); + IPublishedContent? entityContent = _contentCache.GetById(entity.Id); if (entityContent is null) { return; } // Get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures) - var defaultCultures = new Lazy(() => entityContent.AncestorsOrSelf().FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() ?? Array.Empty()); + var defaultCultures = new Lazy(() => entityContent.AncestorsOrSelf(_contentCache, _navigationQueryService).FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() ?? Array.Empty()); // Get all language ISO codes (in case we're dealing with invariant content with variant ancestors) var languageIsoCodes = new Lazy(() => _localizationService.GetAllLanguages().Select(x => x.IsoCode).ToArray()); - foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_variationContextAccessor)) + foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_variationContextAccessor, _contentCache, _navigationQueryService)) { // If this entity defines specific cultures, use those instead of the default ones IEnumerable cultures = publishedContent.Cultures.Any() ? publishedContent.Cultures.Keys : defaultCultures.Value; @@ -59,7 +64,7 @@ namespace Umbraco.Cms.Infrastructure.Routing { try { - var route = contentCache?.GetRouteById(publishedContent.Id, culture); + var route = _contentCache.GetRouteById(publishedContent.Id, culture); if (IsValidRoute(route)) { oldRoutes[(publishedContent.Id, culture)] = (publishedContent.Key, route); @@ -69,7 +74,7 @@ namespace Umbraco.Cms.Infrastructure.Routing // Retry using all languages, if this is invariant but has a variant ancestor. foreach (string languageIsoCode in languageIsoCodes.Value) { - route = contentCache?.GetRouteById(publishedContent.Id, languageIsoCode); + route = _contentCache.GetRouteById(publishedContent.Id, languageIsoCode); if (IsValidRoute(route)) { oldRoutes[(publishedContent.Id, languageIsoCode)] = (publishedContent.Key, route); diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs index 4455b01df3..3c1495f9c1 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -565,8 +565,8 @@ namespace Umbraco.Cms.Infrastructure.Scoping TryFinally( HandleScopedFileSystems, - HandleScopedNotifications, HandleScopeContext, + HandleScopedNotifications, HandleDetachedScopes); } diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 708538f49d..32e5252160 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -23,9 +23,9 @@ public class MemberUserStore : UmbracoUserStore /// Initializes a new instance of the class for the members identity store @@ -34,53 +34,26 @@ public class MemberUserStore : UmbracoUserStoreThe mapper for properties /// The scope provider /// The error describer - /// The published snapshot accessor /// The external login service /// The two factor login service + /// [ActivatorUtilitiesConstructor] public MemberUserStore( IMemberService memberService, IUmbracoMapper mapper, ICoreScopeProvider scopeProvider, IdentityErrorDescriber describer, - IPublishedSnapshotAccessor publishedSnapshotAccessor, IExternalLoginWithKeyService externalLoginService, - ITwoFactorLoginService twoFactorLoginService) + ITwoFactorLoginService twoFactorLoginService, + IPublishedMemberCache memberCache) : base(describer) { _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); - _publishedSnapshotAccessor = publishedSnapshotAccessor; _externalLoginService = externalLoginService; _twoFactorLoginService = twoFactorLoginService; - } - - [Obsolete("Use ctor with IExternalLoginWithKeyService and ITwoFactorLoginService param")] - public MemberUserStore( - IMemberService memberService, - IUmbracoMapper mapper, - ICoreScopeProvider scopeProvider, - IdentityErrorDescriber describer, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IExternalLoginWithKeyService externalLoginService) - : this(memberService, mapper, scopeProvider, describer, publishedSnapshotAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) - { - } - - [Obsolete("Use ctor with IExternalLoginWithKeyService and ITwoFactorLoginService param")] - public MemberUserStore( - IMemberService memberService, - IUmbracoMapper mapper, - ICoreScopeProvider scopeProvider, - IdentityErrorDescriber describer, - IPublishedSnapshotAccessor publishedSnapshotAccessor) - : this(memberService, mapper, scopeProvider, describer, publishedSnapshotAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) - { + _memberCache = memberCache; } /// @@ -319,8 +292,7 @@ public class MemberUserStore : UmbracoUserStore diff --git a/src/Umbraco.PublishedCache.HybridCache/DatabaseCacheRebuilder.cs b/src/Umbraco.PublishedCache.HybridCache/DatabaseCacheRebuilder.cs new file mode 100644 index 0000000000..8d2fe9a04f --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/DatabaseCacheRebuilder.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Infrastructure.HybridCache.Persistence; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +internal class DatabaseCacheRebuilder : IDatabaseCacheRebuilder +{ + private readonly IDatabaseCacheRepository _databaseCacheRepository; + + public DatabaseCacheRebuilder(IDatabaseCacheRepository databaseCacheRepository) + { + _databaseCacheRepository = databaseCacheRepository; + } + + public void Rebuild() => _databaseCacheRepository.Rebuild(); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs index 984fcbe110..ec7c233cb7 100644 --- a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs @@ -44,6 +44,7 @@ public static class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(s => { IOptions options = s.GetRequiredService>(); @@ -62,8 +63,9 @@ public static class UmbracoBuilderExtensions builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); builder.AddCacheSeeding(); return builder; } diff --git a/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs b/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs index 2723a281c2..d978919a65 100644 --- a/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs +++ b/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs @@ -1,7 +1,12 @@ -using Umbraco.Cms.Core; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Infrastructure.HybridCache.Services; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.HybridCache; @@ -34,31 +39,86 @@ public sealed class DocumentCache : IPublishedContentCache public IPublishedContentType? GetContentType(string alias) => _publishedContentTypeCache.Get(PublishedItemType.Content, alias); - public IPublishedContentType? GetContentType(Guid key) => _publishedContentTypeCache.Get(PublishedItemType.Content, key); - // FIXME: These need to be refactored when removing nucache - // Thats the time where we can change the IPublishedContentCache interface. + // TODO: These are all obsolete and should be removed - public IPublishedContent? GetById(bool preview, Udi contentId) => throw new NotImplementedException(); + [Obsolete("Scheduled for removal in v17")] + public IPublishedContent? GetById(bool preview, Udi contentId) + { + if(contentId is not GuidUdi guidUdi) + { + throw new NotSupportedException("Only GuidUdi is supported"); + } - public IPublishedContent? GetById(Udi contentId) => throw new NotImplementedException(); + return GetById(preview, guidUdi.Guid); + } - public IEnumerable GetAtRoot(bool preview, string? culture = null) => throw new NotImplementedException(); + [Obsolete("Scheduled for removal in v17")] + public IPublishedContent? GetById(Udi contentId) + { + if(contentId is not GuidUdi guidUdi) + { + throw new NotSupportedException("Only GuidUdi is supported"); + } - public IEnumerable GetAtRoot(string? culture = null) => throw new NotImplementedException(); + return GetById(guidUdi.Guid); + } - public bool HasContent(bool preview) => throw new NotImplementedException(); + [Obsolete("Scheduled for removal, use IDocumentNavigationQueryService instead in v17")] + public IEnumerable GetAtRoot(bool preview, string? culture = null) + { + IDocumentNavigationQueryService navigationService = StaticServiceProvider.Instance.GetRequiredService(); + navigationService.TryGetRootKeys(out IEnumerable rootKeys); - public bool HasContent() => throw new NotImplementedException(); + IEnumerable rootContent = rootKeys.Select(key => GetById(preview, key)).WhereNotNull(); + return culture is null ? rootContent : rootContent.Where(x => x.IsInvariantOrHasCulture(culture)); + } - public IEnumerable GetByContentType(IPublishedContentType contentType) => throw new NotImplementedException(); + [Obsolete("Scheduled for removal, use IDocumentNavigationQueryService instead in v17")] + public IEnumerable GetAtRoot(string? culture = null) + { + IDocumentNavigationQueryService navigationService = StaticServiceProvider.Instance.GetRequiredService(); + navigationService.TryGetRootKeys(out IEnumerable rootKeys); - public IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException(); + IEnumerable rootContent = rootKeys.Select(key => GetById(key)).WhereNotNull(); + return culture is null ? rootContent : rootContent.Where(x => x.IsInvariantOrHasCulture(culture)); + } - public IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException(); + [Obsolete("Scheduled for removal in v17")] + public bool HasContent(bool preview) => HasContent(); - public string? GetRouteById(bool preview, int contentId, string? culture = null) => throw new NotImplementedException(); + [Obsolete("Scheduled for removal in v17")] + public bool HasContent() => StaticServiceProvider.Instance.GetRequiredService().HasAny(); - public string? GetRouteById(int contentId, string? culture = null) => throw new NotImplementedException(); + [Obsolete] + public IEnumerable GetByContentType(IPublishedContentType contentType) + => _documentCacheService.GetByContentType(contentType); + + [Obsolete("Use IDocumentUrlService.GetDocumentKeyByRoute instead, scheduled for removal in v17")] + public IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) + { + IDocumentUrlService documentUrlService = StaticServiceProvider.Instance.GetRequiredService(); + Guid? key = documentUrlService.GetDocumentKeyByRoute(route, culture, null, preview); + return key is not null ? GetById(preview, key.Value) : null; + } + + [Obsolete("Use IDocumentUrlService.GetDocumentKeyByRoute instead, scheduled for removal in v17")] + public IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) + { + IDocumentUrlService documentUrlService = StaticServiceProvider.Instance.GetRequiredService(); + Guid? key = documentUrlService.GetDocumentKeyByRoute(route, culture, null, false); + return key is not null ? GetById(key.Value) : null; + } + + [Obsolete("Use IDocumentUrlService.GetDocumentKeyByRoute instead, scheduled for removal in v17")] + public string? GetRouteById(bool preview, int contentId, string? culture = null) + { + IDocumentUrlService documentUrlService = StaticServiceProvider.Instance.GetRequiredService(); + IPublishedContent? content = GetById(preview, contentId); + return content is not null ? documentUrlService.GetLegacyRouteFormat(content.Key, culture, preview) : null; + } + + [Obsolete("Use IDocumentUrlService.GetDocumentKeyByRoute instead, scheduled for removal in v17")] + public string? GetRouteById(int contentId, string? culture = null) => GetRouteById(false, contentId, culture); } diff --git a/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs index accc962c5c..e263485cf4 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Core.Models; +using StackExchange.Profiling.Internal; +using Umbraco.Cms.Core.Media.EmbedProviders; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; @@ -17,7 +19,14 @@ internal class CacheNodeFactory : ICacheNodeFactory public ContentCacheNode ToContentCacheNode(IContent content, bool preview) { - ContentData contentData = GetContentData(content, !preview, preview ? content.TemplateId : content.PublishTemplateId); + + + ContentData contentData = GetContentData( + content, + GetPublishedValue(content, preview), + GetTemplateId(content, preview), + content.PublishCultureInfos!.Values.Select(x=>x.Culture).ToHashSet() + ); return new ContentCacheNode { Id = content.Id, @@ -31,9 +40,39 @@ internal class CacheNodeFactory : ICacheNodeFactory }; } + private bool GetPublishedValue(IContent content, bool preview) + { + switch (content.PublishedState) + { + case PublishedState.Published: + return preview; + case PublishedState.Publishing: + return preview is false || content.Published; // The type changes after this operation + case PublishedState.Unpublished: + case PublishedState.Unpublishing: + default: + return false; + } + } + + private int? GetTemplateId(IContent content, bool preview) + { + switch (content.PublishedState) + { + case PublishedState.Published: + return preview ? content.TemplateId : content.PublishTemplateId; + case PublishedState.Publishing: + return content.TemplateId;// The type changes after this operation is we need to read the draft values + case PublishedState.Unpublished: + case PublishedState.Unpublishing: + default: + return null; + } + } + public ContentCacheNode ToContentCacheNode(IMedia media) { - ContentData contentData = GetContentData(media, false, null); + ContentData contentData = GetContentData(media, false, null, new HashSet()); return new ContentCacheNode { Id = media.Id, @@ -47,7 +86,7 @@ internal class CacheNodeFactory : ICacheNodeFactory }; } - private ContentData GetContentData(IContentBase content, bool published, int? templateId) + private ContentData GetContentData(IContentBase content, bool published, int? templateId, ISet publishedCultures) { var propertyData = new Dictionary(); foreach (IProperty prop in content.Properties) @@ -62,7 +101,15 @@ internal class CacheNodeFactory : ICacheNodeFactory } // note: at service level, invariant is 'null', but here invariant becomes 'string.Empty' - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; + if (published && (string.IsNullOrEmpty(pvalue.Culture) is false && publishedCultures.Contains(pvalue.Culture) is false)) + { + continue; + } + + var value = published + ? pvalue.PublishedValue + : pvalue.EditedValue; + if (value != null) { pdatas.Add(new PropertyData diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs index a38c0408a1..a155b3b4eb 100644 --- a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs +++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs @@ -1,4 +1,5 @@ -using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.PublishedContent; @@ -17,7 +18,8 @@ internal sealed class CacheRefreshingNotificationHandler : INotificationAsyncHandler, INotificationAsyncHandler, INotificationAsyncHandler, - INotificationAsyncHandler + INotificationAsyncHandler, + INotificationAsyncHandler { private readonly IDocumentCacheService _documentCacheService; private readonly IMediaCacheService _mediaCacheService; @@ -50,7 +52,7 @@ internal sealed class CacheRefreshingNotificationHandler : { foreach (IContent deletedEntity in notification.DeletedEntities) { - await RefreshElementsCacheAsync(deletedEntity); + RemoveFromElementsCache(deletedEntity); await _documentCacheService.DeleteItemAsync(deletedEntity); } } @@ -65,7 +67,7 @@ internal sealed class CacheRefreshingNotificationHandler : { foreach (IMedia deletedEntity in notification.DeletedEntities) { - await RefreshElementsCacheAsync(deletedEntity); + RemoveFromElementsCache(deletedEntity); await _mediaCacheService.DeleteItemAsync(deletedEntity); } } @@ -76,6 +78,8 @@ internal sealed class CacheRefreshingNotificationHandler : IEnumerable childRelations = _relationService.GetByChild(content); var ids = parentRelations.Select(x => x.ChildId).Concat(childRelations.Select(x => x.ParentId)).ToHashSet(); + // We need to add ourselves to the list of ids to clear + ids.Add(content.Id); foreach (var id in ids) { if (await _documentCacheService.HasContentByIdAsync(id) is false) @@ -92,20 +96,33 @@ internal sealed class CacheRefreshingNotificationHandler : foreach (IPublishedProperty publishedProperty in publishedContent.Properties) { var property = (PublishedProperty) publishedProperty; - if (property.ReferenceCacheLevel != PropertyCacheLevel.Elements) + if (property.ReferenceCacheLevel is PropertyCacheLevel.Elements + || property.PropertyType.DeliveryApiCacheLevel is PropertyCacheLevel.Elements + || property.PropertyType.DeliveryApiCacheLevelForExpansion is PropertyCacheLevel.Elements) { - continue; + _elementsCache.ClearByKey(property.ValuesCacheKey); } - - _elementsCache.ClearByKey(property.ValuesCacheKey); } } } + private void RemoveFromElementsCache(IUmbracoEntity content) + { + // ClearByKey clears by "startsWith" so we'll clear by the cachekey prefix + contentKey + // This will clear any and all properties for this content item, this is important because + // we cannot resolve the PublishedContent for this entity since it and its content type is deleted. + _elementsCache.ClearByKey(GetContentWideCacheKey(content.Key, true)); + _elementsCache.ClearByKey(GetContentWideCacheKey(content.Key, false)); + } + + private string GetContentWideCacheKey(Guid contentKey, bool isPreviewing) => isPreviewing + ? CacheKeys.PreviewPropertyCacheKeyPrefix + contentKey + : CacheKeys.PropertyCacheKeyPrefix + contentKey; + public Task HandleAsync(ContentTypeRefreshedNotification notification, CancellationToken cancellationToken) { const ContentTypeChangeTypes types // only for those that have been refreshed - = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther | ContentTypeChangeTypes.Remove; + = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; var contentTypeIds = notification.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id) .ToArray(); @@ -121,4 +138,14 @@ internal sealed class CacheRefreshingNotificationHandler : return Task.CompletedTask; } + + public Task HandleAsync(ContentTypeDeletedNotification notification, CancellationToken cancellationToken) + { + foreach (IContentType deleted in notification.DeletedEntities) + { + _publishedContentTypeCache.ClearContentType(deleted.Id); + } + + return Task.CompletedTask; + } } diff --git a/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs b/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs index 645f23b12d..2586744ff1 100644 --- a/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs +++ b/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs @@ -1,11 +1,13 @@ using System.Text; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.HybridCache; @@ -77,8 +79,8 @@ internal class PublishedContent : PublishedContentBase { get { - var documentNavigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); - var idKeyMap = StaticServiceProvider.Instance.GetRequiredService(); + IDocumentNavigationQueryService documentNavigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); + IIdKeyMap idKeyMap = StaticServiceProvider.Instance.GetRequiredService(); if (documentNavigationQueryService.TryGetAncestorsOrSelfKeys(Key, out var ancestorsOrSelfKeys)) @@ -86,7 +88,7 @@ internal class PublishedContent : PublishedContentBase var sb = new StringBuilder("-1"); foreach (Guid ancestorsOrSelfKey in ancestorsOrSelfKeys.Reverse()) { - var idAttempt = idKeyMap.GetIdForKey(ancestorsOrSelfKey, GetObjectType()); + Attempt idAttempt = idKeyMap.GetIdForKey(ancestorsOrSelfKey, GetObjectType()); if (idAttempt.Success) { sb.AppendFormat(",{0}", idAttempt.Result); @@ -130,12 +132,31 @@ internal class PublishedContent : PublishedContentBase // Needed for publishedProperty internal IVariationContextAccessor VariationContextAccessor { get; } - public override int Level { get; } = 0; + [Obsolete("Use the INavigationQueryService instead, scheduled for removal in v17")] + public override int Level + { + get + { + INavigationQueryService? navigationQueryService = null; + switch (_contentNode.ContentType.ItemType) + { + case PublishedItemType.Content: + navigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); + break; + case PublishedItemType.Media: + navigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); + break; + default: + throw new NotImplementedException("Level is not implemented for " + _contentNode.ContentType.ItemType); + } - public override IEnumerable ChildrenForAllCultures { get; } = Enumerable.Empty(); - - public override IPublishedContent? Parent { get; } = null!; + navigationQueryService.TryGetLevel(Key, out int level); + return level; + } + } + [Obsolete("Please use TryGetParentKey() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")] + public override IPublishedContent? Parent => GetParent(); /// public override IReadOnlyDictionary Cultures @@ -238,4 +259,26 @@ internal class PublishedContent : PublishedContentBase // = depends on the culture return _contentNode.HasPublishedCulture(culture); } + + private IPublishedContent? GetParent() + { + INavigationQueryService? navigationQueryService; + IPublishedCache? publishedCache; + + switch (ContentType.ItemType) + { + case PublishedItemType.Content: + publishedCache = StaticServiceProvider.Instance.GetRequiredService(); + navigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); + break; + case PublishedItemType.Media: + publishedCache = StaticServiceProvider.Instance.GetRequiredService(); + navigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); + break; + default: + throw new NotImplementedException("Level is not implemented for " + ContentType.ItemType); + } + + return this.Parent(publishedCache, navigationQueryService); + } } diff --git a/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs b/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs index 91e69d9ed7..85b2577ade 100644 --- a/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs +++ b/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs @@ -80,10 +80,10 @@ internal class PublishedProperty : PublishedPropertyBase { if (previewing) { - return "Cache.Property.CacheValues[D:" + contentUid + ":" + typeAlias + "]"; + return CacheKeys.PreviewPropertyCacheKeyPrefix + contentUid + ":" + typeAlias + "]"; } - return "Cache.Property.CacheValues[P:" + contentUid + ":" + typeAlias + "]"; + return CacheKeys.PropertyCacheKeyPrefix + contentUid + ":" + typeAlias + "]"; } // determines whether a property has value diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs index b91ea182f2..5e6b253e9b 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; -using Microsoft.Extensions.Caching.Hybrid; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -96,6 +94,17 @@ internal sealed class DocumentCacheService : IDocumentCacheService return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory);; } + public IEnumerable GetByContentType(IPublishedContentType contentType) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + IEnumerable nodes = _databaseCacheRepository.GetContentByContentTypeKey([contentType.Key]); + scope.Complete(); + + return nodes + .Select(x => _publishedContentFactory.ToIPublishedContent(x, x.IsDraft).CreateModel(_publishedModelFactory)) + .WhereNotNull(); + } + public async Task SeedAsync(CancellationToken cancellationToken) { using ICoreScope scope = _scopeProvider.CreateCoreScope(); diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DomainCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DomainCacheService.cs index 9efc749d6b..c957f17231 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/DomainCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DomainCacheService.cs @@ -17,6 +17,7 @@ public class DomainCacheService : IDomainCacheService private readonly IDomainService _domainService; private readonly ICoreScopeProvider _coreScopeProvider; private readonly ConcurrentDictionary _domains; + private bool _initialized = false; public DomainCacheService(IDomainService domainService, ICoreScopeProvider coreScopeProvider) { @@ -27,14 +28,26 @@ public class DomainCacheService : IDomainCacheService public IEnumerable GetAll(bool includeWildcards) { + InitializeIfMissing(); return includeWildcards == false ? _domains.Select(x => x.Value).Where(x => x.IsWildcard == false).OrderBy(x => x.SortOrder) : _domains.Select(x => x.Value).OrderBy(x => x.SortOrder); } + private void InitializeIfMissing() + { + if (_initialized) + { + return; + } + _initialized = true; + LoadDomains(); + } + /// public IEnumerable GetAssigned(int documentId, bool includeWildcards = false) { + InitializeIfMissing(); // probably this could be optimized with an index // but then we'd need a custom DomainStore of some sort IEnumerable list = _domains.Values.Where(x => x.ContentId == documentId); @@ -48,7 +61,10 @@ public class DomainCacheService : IDomainCacheService /// public bool HasAssigned(int documentId, bool includeWildcards = false) - => documentId > 0 && GetAssigned(documentId, includeWildcards).Any(); + { + InitializeIfMissing(); + return documentId > 0 && GetAssigned(documentId, includeWildcards).Any(); + } public void Refresh(DomainCacheRefresher.JsonPayload[] payloads) { @@ -100,12 +116,19 @@ public class DomainCacheService : IDomainCacheService private void LoadDomains() { - IEnumerable domains = _domainService.GetAll(true); - foreach (Domain domain in domains - .Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false) - .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId!.Value, x.LanguageIsoCode!, x.IsWildcard, x.SortOrder))) + using (ICoreScope scope = _coreScopeProvider.CreateCoreScope()) { - _domains.AddOrUpdate(domain.Id, domain, (key, oldValue) => domain); + scope.ReadLock(Constants.Locks.Domains); + IEnumerable domains = _domainService.GetAll(true); + foreach (Domain domain in domains + .Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false) + .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId!.Value, x.LanguageIsoCode!, x.IsWildcard, x.SortOrder))) + { + _domains.AddOrUpdate(domain.Id, domain, (key, oldValue) => domain); + } + scope.Complete(); } + + } } diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs index 280e0e97f0..95b710e739 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs @@ -18,4 +18,6 @@ public interface IDocumentCacheService Task DeleteItemAsync(IContentBase content); void Rebuild(IReadOnlyCollection contentTypeKeys); + + internal IEnumerable GetByContentType(IPublishedContentType contentType); } diff --git a/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj b/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj index c37535f762..a16dd4d5a0 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj +++ b/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj @@ -22,6 +22,12 @@ <_Parameter1>Umbraco.Tests + + <_Parameter1>Umbraco.Tests.Common + + + <_Parameter1>Umbraco.Tests.UnitTests + <_Parameter1>Umbraco.Tests.Integration diff --git a/src/Umbraco.PublishedCache.NuCache/CacheKeys.cs b/src/Umbraco.PublishedCache.NuCache/CacheKeys.cs deleted file mode 100644 index 2d718a0dfd..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/CacheKeys.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -internal static class CacheKeys -{ - public static string PublishedContentChildren(Guid contentUid, bool previewing) - { - if (previewing) - { - return "NuCache.Content.Children[D::" + contentUid + "]"; - } - - return "NuCache.Content.Children[P::" + contentUid + "]"; - } - - public static string ContentCacheRoots(bool previewing) - { - if (previewing) - { - return "NuCache.ContentCache.Roots[D:]"; - } - - return "NuCache.ContentCache.Roots[P:]"; - } - - public static string MediaCacheRoots(bool previewing) - { - if (previewing) - { - return "NuCache.MediaCache.Roots[D:]"; - } - - return "NuCache.MediaCache.Roots[P:]"; - } - - public static string PublishedContentAsPreviewing(Guid contentUid) => - "NuCache.Content.AsPreviewing[" + contentUid + "]"; - - public static string ProfileName(int userId) => "NuCache.Profile.Name[" + userId + "]"; - - public static string PropertyCacheValues(Guid contentUid, string typeAlias, bool previewing) - { - if (previewing) - { - return "NuCache.Property.CacheValues[D:" + contentUid + ":" + typeAlias + "]"; - } - - return "NuCache.Property.CacheValues[P:" + contentUid + ":" + typeAlias + "]"; - } - - // routes still use int id and not Guid uid, because routable nodes must have - // a valid ID in the database at that point, whereas content and properties - // may be virtual (and not in umbracoNode). - public static string ContentCacheRouteByContent(int id, bool previewing, string? culture) - { - if (string.IsNullOrEmpty(culture)) - { - if (previewing) - { - return "NuCache.ContentCache.RouteByContent[D:" + id +"]"; - } - - return "NuCache.ContentCache.RouteByContent[P:" + id + "]"; - } - else if (previewing) - { - return "NuCache.ContentCache.RouteByContent[D:" + id + "-L:" + culture + "]"; - } - return "NuCache.ContentCache.RouteByContent[P:" + id + "-L:" + culture + "]"; - } - - public static string ContentCacheContentByRoute(string route, bool previewing, string? culture) - { - if (string.IsNullOrEmpty(culture)) - { - if (previewing) - { - return "NuCache.ContentCache.ContentByRoute[D:" + route + "]"; - } - - return "NuCache.ContentCache.ContentByRoute[P:" + route + "]"; - } - else if (previewing) - { - return "NuCache.ContentCache.ContentByRoute[D:" + route + "-L:" + culture + "]"; - } - - return "NuCache.ContentCache.ContentByRoute[P:" + route + "-L:" + culture + "]"; - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs deleted file mode 100644 index 8c4b369234..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs +++ /dev/null @@ -1,371 +0,0 @@ -using System.Globalization; -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.Navigable; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -public class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigableData, IDisposable -{ - private readonly IDomainCache _domainCache; - private readonly IAppCache? _elementsCache; - private readonly GlobalSettings _globalSettings; - private readonly ContentStore.Snapshot _snapshot; - private readonly IAppCache _snapshotCache; - private readonly IVariationContextAccessor _variationContextAccessor; - - public void Dispose() => _snapshot.Dispose(); - - // TODO: figure this out - // after the current snapshot has been resync-ed - // it's too late for UmbracoContext which has captured previewDefault and stuff into these ctor vars - // but, no, UmbracoContext returns snapshot.Content which comes from elements SO a resync should create a new cache - public ContentCache( - bool previewDefault, - ContentStore.Snapshot snapshot, - IAppCache snapshotCache, - IAppCache? elementsCache, - IDomainCache domainCache, - IOptions globalSettings, - IVariationContextAccessor variationContextAccessor) - : base(variationContextAccessor, previewDefault) - { - _snapshot = snapshot; - _snapshotCache = snapshotCache; - _elementsCache = elementsCache; - _domainCache = domainCache ?? throw new ArgumentNullException(nameof(domainCache)); - _globalSettings = globalSettings.Value; - _variationContextAccessor = variationContextAccessor; - } - - private bool HideTopLevelNodeFromPath => _globalSettings.HideTopLevelNodeFromPath; - - // routes can be - // "/" - // "123/" - // "/path/to/node" - // "123/path/to/node" - - // at the moment we try our best to be backward compatible, but really, - // should get rid of hideTopLevelNode and other oddities entirely, eventually - public IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) => - GetByRoute(PreviewDefault, route, hideTopLevelNode, culture); - - public Task GetByIdAsync(int id, bool preview = false) => throw new NotImplementedException(); - - public Task GetByIdAsync(Guid key, bool preview = false) => throw new NotImplementedException(); - - public Task HasByIdAsync(int id, bool preview = false) => throw new NotImplementedException(); - - public IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) - { - if (route == null) - { - throw new ArgumentNullException(nameof(route)); - } - - IAppCache? cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing - ? _elementsCache - : _snapshotCache; - var key = CacheKeys.ContentCacheContentByRoute(route, preview, culture); - return cache?.GetCacheItem(key, () => GetByRouteInternal(preview, route, hideTopLevelNode, culture)); - } - - private IPublishedContent? GetByRouteInternal(bool preview, string route, bool? hideTopLevelNode, string? culture) - { - hideTopLevelNode = hideTopLevelNode ?? HideTopLevelNodeFromPath; // default = settings - - // the route always needs to be lower case because we only store the urlName attribute in lower case - route = route.ToLowerInvariant(); - - var pos = route.IndexOf('/'); - var path = pos == 0 ? route : route.Substring(pos); - var startNodeId = pos == 0 ? 0 : int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture); - var parts = path.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries); - - IPublishedContent? content; - - if ((!_globalSettings.ForceCombineUrlPathLeftToRight - && CultureInfo.GetCultureInfo(culture ?? _globalSettings.DefaultUILanguage).TextInfo.IsRightToLeft)) - { - parts = parts.Reverse().ToArray(); - } - - if (startNodeId > 0) - { - // if in a domain then start with the root node of the domain - // and follow the path - // note: if domain has a path (eg example.com/en) which is not recommended anymore - // then /en part of the domain is basically ignored here... - content = GetById(preview, startNodeId); - content = FollowRoute(content, parts, 0, culture); - } - else if (parts.Length == 0) - { - // if not in a domain, and path is empty - what is the default page? - // let's say it is the first one in the tree, if any -- order by sortOrder - content = GetAtRoot(preview).FirstOrDefault(); - } - else - { - // if not in a domain... - // hideTopLevelNode = support legacy stuff, look for /*/path/to/node - // else normal, look for /path/to/node - content = hideTopLevelNode.Value - ? GetAtRoot(preview) - .SelectMany(x => x.Children(_variationContextAccessor, culture)!) // Do we suppress here? - .FirstOrDefault(x => x.UrlSegment(_variationContextAccessor, culture) == parts[0]) - : GetAtRoot(preview) - .FirstOrDefault(x => x.UrlSegment(_variationContextAccessor, culture) == parts[0]); - content = FollowRoute(content, parts, 1, culture); - } - - // if hideTopLevelNodePath is true then for URL /foo we looked for /*/foo - // but maybe that was the URL of a non-default top-level node, so we also - // have to look for /foo (see note in ApplyHideTopLevelNodeFromPath). - if (content == null && hideTopLevelNode.Value && parts.Length == 1) - { - content = GetAtRoot(preview) - .FirstOrDefault(x => x.UrlSegment(_variationContextAccessor, culture) == parts[0]); - } - - return content; - } - - public string? GetRouteById(int contentId, string? culture = null) => - GetRouteById(PreviewDefault, contentId, culture); - - public string? GetRouteById(bool preview, int contentId, string? culture = null) - { - IAppCache? cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing - ? _elementsCache - : _snapshotCache; - var key = CacheKeys.ContentCacheRouteByContent(contentId, preview, culture); - return cache?.GetCacheItem(key, () => GetRouteByIdInternal(preview, contentId, null, culture)); - } - - private string? GetRouteByIdInternal(bool preview, int contentId, bool? hideTopLevelNode, string? culture) - { - IPublishedContent? node = GetById(preview, contentId); - if (node == null) - { - return null; - } - - hideTopLevelNode = hideTopLevelNode ?? HideTopLevelNodeFromPath; // default = settings - - // walk up from that node until we hit a node with a domain, - // or we reach the content root, collecting URLs in the way - var pathParts = new List(); - IPublishedContent? content = node; - var urlSegment = content.UrlSegment(_variationContextAccessor, culture); - var hasDomains = _domainCache.GetAssignedWithCulture(culture, content.Id); - - // content is null at root - while (hasDomains == false && content != null) - { - // no segment indicates this is not published when this is a variant - if (urlSegment.IsNullOrWhiteSpace()) - { - return null; - } - - pathParts.Add(urlSegment!); - - // move to parent node - content = content.Parent; - if (content != null) - { - urlSegment = content.UrlSegment(_variationContextAccessor, culture); - } - - hasDomains = content != null && _domainCache.GetAssignedWithCulture(culture, content.Id); - } - - // at this point this will be the urlSegment of the root, no segment indicates this is not published when this is a variant - if (urlSegment.IsNullOrWhiteSpace()) - { - return null; - } - - // no domain, respect HideTopLevelNodeFromPath for legacy purposes - if (hasDomains == false && hideTopLevelNode.Value) - { - ApplyHideTopLevelNodeFromPath(node, pathParts, preview); - } - - // assemble the route- We only have to reverse for left to right languages - if ((_globalSettings.ForceCombineUrlPathLeftToRight - || !CultureInfo.GetCultureInfo(culture ?? _globalSettings.DefaultUILanguage).TextInfo.IsRightToLeft)) - { - pathParts.Reverse(); - } - - var path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc - - // prefix the root node id containing the domain if it exists (this is a standard way of creating route paths) - // and is done so that we know the ID of the domain node for the path - var route = (content?.Id.ToString(CultureInfo.InvariantCulture) ?? string.Empty) + path; - - return route; - } - - private IPublishedContent? FollowRoute(IPublishedContent? content, IReadOnlyList parts, int start, string? culture) - { - var i = start; - while (content != null && i < parts.Count) - { - var part = parts[i++]; - content = content.Children(_variationContextAccessor, culture)?.FirstOrDefault(x => - { - var urlSegment = x.UrlSegment(_variationContextAccessor, culture); - return urlSegment == part; - }); - } - - return content; - } - - private void ApplyHideTopLevelNodeFromPath(IPublishedContent content, IList segments, bool preview) - { - // in theory if hideTopLevelNodeFromPath is true, then there should be only one - // top-level node, or else domains should be assigned. but for backward compatibility - // we add this check - we look for the document matching "/" and if it's not us, then - // we do not hide the top level path - // it has to be taken care of in GetByRoute too so if - // "/foo" fails (looking for "/*/foo") we try also "/foo". - // this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but - // that's the way it works pre-4.10 and we try to be backward compat for the time being - if (content.Parent == null) - { - IPublishedContent? rootNode = GetByRoute(preview, "/", true); - if (rootNode == null) - { - throw new Exception("Failed to get node at /. This might be because you're trying to publish a variant, with no domains setup"); - } - - // remove only if we're the default node - if (rootNode.Id == content.Id) - { - segments.RemoveAt(segments.Count - 1); - } - } - else - { - segments.RemoveAt(segments.Count - 1); - } - } - - public override IPublishedContent? GetById(bool preview, int contentId) - { - ContentNode? node = _snapshot.Get(contentId); - return GetNodePublishedContent(node, preview); - } - - public override IPublishedContent? GetById(bool preview, Guid contentId) - { - ContentNode? node = _snapshot.Get(contentId); - return GetNodePublishedContent(node, preview); - } - - public override IPublishedContent? GetById(bool preview, Udi contentId) - { - var guidUdi = contentId as GuidUdi; - if (guidUdi == null) - { - throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId)); - } - - if (guidUdi.EntityType != Constants.UdiEntityType.Document) - { - throw new ArgumentException($"Udi entity type must be \"{Constants.UdiEntityType.Document}\".", nameof(contentId)); - } - - return GetById(preview, guidUdi.Guid); - } - - public override bool HasById(bool preview, int contentId) - { - ContentNode? n = _snapshot.Get(contentId); - if (n == null) - { - return false; - } - - return preview || n.PublishedModel != null; - } - - IEnumerable INavigableData.GetAtRoot(bool preview) => GetAtRoot(preview); - - public override IEnumerable GetAtRoot(bool preview, string? culture = null) - { - // handle context culture for variant - if (culture == null) - { - culture = _variationContextAccessor?.VariationContext?.Culture ?? string.Empty; - } - - // _snapshot.GetAtRoot() returns all ContentNode at root - // both .Draft and .Published cannot be null at the same time - // root is already sorted by sortOrder, and does not contain nulls - // - // GetNodePublishedContent may return null if !preview and there is no - // published model, so we need to filter these nulls out - IEnumerable atRoot = _snapshot.GetAtRoot() - .Select(n => GetNodePublishedContent(n, preview)) - .WhereNotNull(); - - // if a culture is specified, we must ensure that it is avail/published - if (culture != "*") - { - atRoot = atRoot.Where(x => x.IsInvariantOrHasCulture(culture)); - } - - return atRoot; - } - - private static IPublishedContent? GetNodePublishedContent(ContentNode? node, bool preview) - { - if (node == null) - { - return null; - } - - // both .Draft and .Published cannot be null at the same time - return preview - ? node.DraftModel ?? GetPublishedContentAsDraft(node.PublishedModel) - : node.PublishedModel; - } - - // gets a published content as a previewing draft, if preview is true - // this is for published content when previewing - private static IPublishedContent? GetPublishedContentAsDraft(IPublishedContent? content /*, bool preview*/) - { - if (content == null /*|| preview == false*/) - { - return null; // content; - } - - // an object in the cache is either an IPublishedContentOrMedia, - // or a model inheriting from PublishedContentExtended - in which - // case we need to unwrap to get to the original IPublishedContentOrMedia. - var inner = PublishedContent.UnwrapIPublishedContent(content); - return inner.AsDraft(); - } - - public override bool HasContent(bool preview) => - preview - ? _snapshot.IsEmpty == false - : _snapshot.GetAtRoot().Any(x => x.PublishedModel != null); - - public override IPublishedContentType? GetContentType(int id) => _snapshot.GetContentType(id); - - public override IPublishedContentType? GetContentType(string alias) => _snapshot.GetContentType(alias); - - public override IPublishedContentType? GetContentType(Guid key) => _snapshot.GetContentType(key); -} diff --git a/src/Umbraco.PublishedCache.NuCache/ContentNode.cs b/src/Umbraco.PublishedCache.NuCache/ContentNode.cs deleted file mode 100644 index 3dce80076a..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/ContentNode.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System.Diagnostics; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -// represents a content "node" ie a pair of draft + published versions -// internal, never exposed, to be accessed from ContentStore (only!) -[DebuggerDisplay("Id: {Id}, Path: {Path}")] -public class ContentNode -{ - // everything that is common to both draft and published versions - // keep this as small as possible -#pragma warning disable IDE1006 // Naming Styles - public readonly int Id; - - private ContentData? _draftData; - - // draft and published version (either can be null, but not both) - // are models not direct PublishedContent instances - private IPublishedContent? _draftModel; - private ContentData? _publishedData; - private IPublishedContent? _publishedModel; - private IPublishedModelFactory? _publishedModelFactory; - private IPublishedSnapshotAccessor? _publishedSnapshotAccessor; - - private IVariationContextAccessor? _variationContextAccessor; - - // special ctor for root pseudo node - public ContentNode() - { - FirstChildContentId = -1; - LastChildContentId = -1; - NextSiblingContentId = -1; - PreviousSiblingContentId = -1; - Path = string.Empty; - } - - // special ctor with no content data - for members - public ContentNode( - int id, - Guid uid, - IPublishedContentType contentType, - int level, - string path, - int sortOrder, - int parentContentId, - DateTime createDate, - int creatorId) - : this() - { - Id = id; - Uid = uid; - ContentType = contentType; - Level = level; - Path = path; - SortOrder = sortOrder; - ParentContentId = parentContentId; - CreateDate = createDate; - CreatorId = creatorId; - } - - public ContentNode( - int id, - Guid uid, - IPublishedContentType contentType, - int level, - string path, - int sortOrder, - int parentContentId, - DateTime createDate, - int creatorId, - ContentData draftData, - ContentData publishedData, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IPublishedModelFactory publishedModelFactory) - : this(id, uid, level, path, sortOrder, parentContentId, createDate, creatorId) => - SetContentTypeAndData(contentType, draftData, publishedData, publishedSnapshotAccessor, variationContextAccessor, publishedModelFactory); - - // 2-phases ctor, phase 1 - public ContentNode( - int id, - Guid uid, - int level, - string path, - int sortOrder, - int parentContentId, - DateTime createDate, - int creatorId) - { - Id = id; - Uid = uid; - Level = level; - Path = path; - SortOrder = sortOrder; - ParentContentId = parentContentId; - FirstChildContentId = -1; - LastChildContentId = -1; - NextSiblingContentId = -1; - PreviousSiblingContentId = -1; - CreateDate = createDate; - CreatorId = creatorId; - } - - // clone - public ContentNode(ContentNode origin, IPublishedModelFactory publishedModelFactory, IPublishedContentType? contentType = null) - { - _publishedModelFactory = publishedModelFactory; - Id = origin.Id; - Uid = origin.Uid; - ContentType = contentType ?? origin.ContentType; - Level = origin.Level; - Path = origin.Path; - SortOrder = origin.SortOrder; - ParentContentId = origin.ParentContentId; - FirstChildContentId = origin.FirstChildContentId; - LastChildContentId = origin.LastChildContentId; - NextSiblingContentId = origin.NextSiblingContentId; - PreviousSiblingContentId = origin.PreviousSiblingContentId; - CreateDate = origin.CreateDate; - CreatorId = origin.CreatorId; - - _draftData = origin._draftData; - _publishedData = origin._publishedData; - _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; - _variationContextAccessor = origin._variationContextAccessor; - } - - public bool HasPublished => _publishedData != null; - - public IPublishedContent? DraftModel => GetModel(ref _draftModel, _draftData); - - public IPublishedContent? PublishedModel => GetModel(ref _publishedModel, _publishedData); - - // two-phase ctor, phase 2 - public void SetContentTypeAndData( - IPublishedContentType contentType, - ContentData? draftData, - ContentData? publishedData, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IPublishedModelFactory publishedModelFactory) - { - ContentType = contentType; - - if (draftData == null && publishedData == null) - { - throw new ArgumentException("Both draftData and publishedData cannot be null at the same time."); - } - - _publishedSnapshotAccessor = publishedSnapshotAccessor; - _variationContextAccessor = variationContextAccessor; - _publishedModelFactory = publishedModelFactory; - - _draftData = draftData; - _publishedData = publishedData; - } - - public bool HasPublishedCulture(string culture) => - _publishedData != null && (_publishedData.CultureInfos?.ContainsKey(culture) ?? false); - - public ContentNodeKit ToKit() => new(this, ContentType.Id, _draftData, _publishedData); - - private IPublishedContent? GetModel(ref IPublishedContent? model, ContentData? contentData) - { - if (model != null) - { - return model; - } - - if (contentData == null) - { - return null; - } - - // create the model - we want to be fast, so no lock here: we may create - // more than 1 instance, but the lock below ensures we only ever return - // 1 unique instance - and locking is a nice explicit way to ensure this - IPublishedContent? m = - new PublishedContent(this, contentData, _publishedSnapshotAccessor, _variationContextAccessor, _publishedModelFactory).CreateModel(_publishedModelFactory); - - // locking 'this' is not a best-practice but ContentNode is internal and - // we know what we do, so it is fine here and avoids allocating an object - lock (this) - { - return model ??= m; - } - } - - public readonly Guid Uid; - public readonly int Level; - public IPublishedContentType ContentType = null!; - public readonly string Path; - public readonly int SortOrder; - public readonly int ParentContentId; - public readonly DateTime CreateDate; - -#pragma warning restore IDE1006 // Naming Styles - - // TODO: Can we make everything readonly?? This would make it easier to debug and be less error prone especially for new developers. - // Once a Node is created and exists in the cache it is readonly so we should be able to make that happen at the API level too. -#pragma warning disable IDE1006 // Naming Styles - public int FirstChildContentId; - public int LastChildContentId; - public int NextSiblingContentId; - public int PreviousSiblingContentId; - public readonly int CreatorId; -#pragma warning restore IDE1006 // Naming Styles -} diff --git a/src/Umbraco.PublishedCache.NuCache/ContentNodeKit.cs b/src/Umbraco.PublishedCache.NuCache/ContentNodeKit.cs deleted file mode 100644 index c47c04b548..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/ContentNodeKit.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - - public struct ContentNodeKit - { - public ContentNode Node { get; } = null!; - - public int ContentTypeId { get; } - - public ContentData? DraftData { get; } - - public ContentData? PublishedData { get; } - - public ContentNodeKit(ContentNode node, int contentTypeId, ContentData? draftData, ContentData? publishedData) - { - Node = node; - ContentTypeId = contentTypeId; - DraftData = draftData; - PublishedData = publishedData; - } - - public static ContentNodeKit Empty { get; } = default(ContentNodeKit); - - public bool IsEmpty => Node == null; - - public bool IsNull => ContentTypeId < 0; - - public static ContentNodeKit Null { get; } = new(null!, -1, null, null); - - public void Build( - IPublishedContentType contentType, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IPublishedModelFactory publishedModelFactory, - bool canBePublished) - { - ContentData? draftData = DraftData; - - // no published data if it cannot be published (eg is masked) - ContentData? publishedData = canBePublished ? PublishedData : null; - - // we *must* have either published or draft data - // if it cannot be published, published data is going to be null - // therefore, ensure that draft data is not - if (draftData == null && !canBePublished) - { - draftData = PublishedData; - } - - Node?.SetContentTypeAndData(contentType, draftData, publishedData, publishedSnapshotAccessor, variationContextAccessor, publishedModelFactory); - } - - public ContentNodeKit Clone(IPublishedModelFactory publishedModelFactory) - => new(new ContentNode(Node, publishedModelFactory), ContentTypeId, DraftData, PublishedData); - - public ContentNodeKit Clone(IPublishedModelFactory publishedModelFactory, ContentData draftData, ContentData publishedData) - => new(new ContentNode(Node, publishedModelFactory), ContentTypeId, draftData, publishedData); -} diff --git a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs deleted file mode 100644 index 0230032dc2..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs +++ /dev/null @@ -1,2077 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using CSharpTest.Net.Collections; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Exceptions; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Infrastructure.PublishedCache.Snap; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -/// -/// Stores content in memory and persists it back to disk -/// -/// -/// -/// Methods in this class suffixed with the term "Locked" means that those methods can only be called within a -/// WriteLock. A WriteLock -/// is acquired by the GetScopedWriteLock method. Locks are not allowed to be recursive. -/// -/// -/// This class's logic is based on the class but has been slightly -/// modified to suit these purposes. -/// -/// -public class ContentStore -{ - private static readonly TimeSpan _monitorTimeout = TimeSpan.FromSeconds(30); - - // TODO: collection trigger (ok for now) - // see SnapDictionary notes - private const long CollectMinGenDelta = 8; - private readonly ConcurrentDictionary _contentKeyToIdMap; - private readonly ConcurrentDictionary> _contentNodes; - private readonly ConcurrentDictionary _contentTypeKeyToIdMap; - private readonly ConcurrentDictionary> _contentTypesByAlias; - - // We must keep separate dictionaries for by id and by alias because we track these in snapshot/layers - // and it is possible that the alias of a content type can be different for the same id in another layer - // whereas the GUID -> INT cross reference can never be different - private readonly ConcurrentDictionary> _contentTypesById; - private readonly ConcurrentQueue _genObjs; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - - private readonly IPublishedModelFactory _publishedModelFactory; - - // this class is an extended version of SnapDictionary - // most of the snapshots management code, etc is an exact copy - // SnapDictionary has unit tests to ensure it all works correctly - // For locking information, see SnapDictionary - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly object _rlocko = new(); - private readonly IVariationContextAccessor _variationContextAccessor; - private readonly object _wlocko = new(); - private Task? _collectTask; - private GenObj? _genObj; - private long _liveGen; - private long _floorGen; - private BPlusTree? _localDb; - private bool _nextGen; - private bool _collectAuto; - private LinkedNode _root; - private List>? _wchanges; - - #region Ctor - - public ContentStore( - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - ILogger logger, - ILoggerFactory loggerFactory, - IPublishedModelFactory publishedModelFactory, - BPlusTree? localDb = null) - { - _publishedSnapshotAccessor = publishedSnapshotAccessor; - _variationContextAccessor = variationContextAccessor; - _logger = logger; - _loggerFactory = loggerFactory; - _publishedModelFactory = publishedModelFactory; - _localDb = localDb; - - _contentNodes = new ConcurrentDictionary>(); - _root = new LinkedNode(new ContentNode(), 0); - _contentTypesById = new ConcurrentDictionary>(); - _contentTypesByAlias = - new ConcurrentDictionary>(StringComparer - .InvariantCultureIgnoreCase); - _contentTypeKeyToIdMap = new ConcurrentDictionary(); - _contentKeyToIdMap = new ConcurrentDictionary(); - - _genObjs = new ConcurrentQueue(); - _genObj = null; // no initial gen exists - _liveGen = _floorGen = 0; - _nextGen = false; // first time, must create a snapshot - _collectAuto = true; // collect automatically by default - } - - #endregion - - #region Classes - - public class Snapshot : IDisposable - { - private readonly GenRef? _genRef; -#if DEBUG - private readonly ILogger _logger; -#endif - private readonly ContentStore _store; - private long _gen; - - // private static int _count; - // private readonly int _thisCount; - internal Snapshot(ContentStore store, GenRef genRef -#if DEBUG - , ILogger logger -#endif - ) - { - _store = store; - _genRef = genRef; - _gen = genRef.Gen; - Interlocked.Increment(ref genRef.GenObj.Count); - - // _thisCount = _count++; -#if DEBUG - _logger = logger; - _logger.LogDebug("Creating snapshot."); -#endif - } - - internal Snapshot(ContentStore store, long gen -#if DEBUG - , ILogger logger -#endif - ) - { - _store = store; - _gen = gen; - -#if DEBUG - _logger = logger; - _logger.LogDebug("Creating live."); -#endif - } - - // this code is here just so you don't try to implement it - // the only way we can iterate over "all" without locking the entire cache forever - // is by shallow cloning the cache, which is quite expensive, so we should probably not do it, - // and implement cache-level indexes - // public IEnumerable GetAll() - // { - // if (_gen < 0) - // throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); - // return _store.GetAll(_gen); - // } - - public bool IsEmpty - { - get - { - if (_gen < 0) - { - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); - } - - return _store.IsEmpty(_gen); - } - } - - public long Gen - { - get - { - if (_gen < 0) - { - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); - } - - return _gen; - } - } - - public void Dispose() - { - if (_gen < 0) - { - return; - } -#if DEBUG - _logger.LogDebug("Dispose snapshot ({Snapshot})", _genRef?.GenObj.Count.ToString() ?? "live"); -#endif - _gen = -1; - if (_genRef != null) - { - Interlocked.Decrement(ref _genRef.GenObj.Count); - } - - GC.SuppressFinalize(this); - } - - public ContentNode? Get(int id) - { - if (_gen < 0) - { - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); - } - - return _store.Get(id, _gen); - } - - public ContentNode? Get(Guid id) - { - if (_gen < 0) - { - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); - } - - return _store.Get(id, _gen); - } - - public IEnumerable GetAtRoot() - { - if (_gen < 0) - { - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); - } - - return _store.GetAtRoot(_gen).WhereNotNull(); - } - - public IEnumerable GetAll() - { - if (_gen < 0) - { - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); - } - - return _store.GetAll(_gen); - } - - public IPublishedContentType? GetContentType(int id) - { - if (_gen < 0) - { - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); - } - - return _store.GetContentType(id, _gen); - } - - public IPublishedContentType? GetContentType(string alias) - { - if (_gen < 0) - { - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); - } - - return _store.GetContentType(alias, _gen); - } - - public IPublishedContentType? GetContentType(Guid key) - { - if (_gen < 0) - { - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); - } - - return _store.GetContentType(key, _gen); - } - } - - #endregion - - #region Locking - - // see notes on SnapDictionary - private readonly string _instanceId = Guid.NewGuid().ToString("N"); - - private class WriteLockInfo - { -#pragma warning disable IDE1006 // Naming Styles - - // This is a field that is used for ref operations - public bool Taken; -#pragma warning restore IDE1006 // Naming Styles - } - - // a scope contextual that represents a locked writer to the dictionary - private class ScopedWriteLock : ScopeContextualBase - { - private readonly WriteLockInfo _lockinfo = new(); - private readonly ContentStore _store; - private int _released; - - public ScopedWriteLock(ContentStore store, bool scoped) - { - _store = store; - store.Lock(_lockinfo, scoped); - } - - public override void Release(bool completed) - { - if (Interlocked.CompareExchange(ref _released, 1, 0) != 0) - { - return; - } - - _store.Release(_lockinfo, completed); - } - } - - // gets a scope contextual representing a locked writer to the dictionary - // TODO: GetScopedWriter? should the dict have a ref onto the scope provider? - public IDisposable? GetScopedWriteLock(ICoreScopeProvider scopeProvider) => - ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); - - private void EnsureLocked() - { - if (!Monitor.IsEntered(_wlocko)) - { - throw new InvalidOperationException("Write lock must be acquried."); - } - } - - private void Lock(WriteLockInfo lockInfo, bool forceGen = false) - { - if (Monitor.IsEntered(_wlocko)) - { - throw new InvalidOperationException("Recursive locks not allowed"); - } - - Monitor.TryEnter(_wlocko, _monitorTimeout, ref lockInfo.Taken); - - if (Monitor.IsEntered(_wlocko) is false) - { - throw new TimeoutException("Could not enter monitor before timeout in content store"); - } - - lock (_rlocko) - { - // see SnapDictionary - try - { - } - finally - { - if (_nextGen == false || forceGen) - { - // because we are changing things, a new generation - // is created, which will trigger a new snapshot - if (_nextGen) - { - _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); - } - - _liveGen += 1; - _nextGen = true; - } - } - } - } - - private void Release(WriteLockInfo lockInfo, bool commit = true) - { - try - { - if (commit == false) - { - lock (_rlocko) - { - // see SnapDictionary - try - { - } - finally - { - _nextGen = false; - _liveGen -= 1; - } - } - - Rollback(_contentNodes); - RollbackRoot(); - Rollback(_contentTypesById); - Rollback(_contentTypesByAlias); - } - else if (_localDb != null && _wchanges != null) - { - foreach (KeyValuePair change in _wchanges) - { - if (change.Value.IsNull) - { - _localDb.TryRemove(change.Key, out ContentNodeKit unused); - } - else - { - _localDb[change.Key] = change.Value; - } - } - - _wchanges = null; - _localDb.Commit(); - } - } - finally - { - if (lockInfo.Taken) - { - Monitor.Exit(_wlocko); - } - } - } - - private void RollbackRoot() - { - if (_root.Gen <= _liveGen) - { - return; - } - - if (_root.Next != null) - { - _root = _root.Next; - } - } - - private void Rollback(ConcurrentDictionary> dictionary) - where TValue : class? - where TKey : notnull - { - foreach (KeyValuePair> item in dictionary) - { - LinkedNode? link = item.Value; - if (link.Gen <= _liveGen) - { - continue; - } - - TKey key = item.Key; - if (link.Next == null) - { - dictionary.TryRemove(key, out link); - } - else - { - dictionary.TryUpdate(key, link.Next, link); - } - } - } - - #endregion - - #region LocalDb - - public void ReleaseLocalDb() - { - var lockInfo = new WriteLockInfo(); - try - { - try - { - // Trying to lock could throw exceptions so always make sure to clean up. - Lock(lockInfo); - } - finally - { - try - { - _localDb?.Dispose(); - } - catch (Exception ex) - { - /* TBD: May already be throwing so don't throw again */ - _logger.LogError(ex, "Error trying to release DB"); - } - finally - { - _localDb = null; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error trying to lock"); - throw; - } - finally - { - Release(lockInfo); - } - } - - private void RegisterChange(int id, ContentNodeKit kit) - { - if (_wchanges == null) - { - _wchanges = new List>(); - } - - _wchanges.Add(new KeyValuePair(id, kit)); - } - - #endregion - - #region Content types - - /// - /// Sets data for new content types - /// - /// - /// - /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock - /// otherwise an exception will occur. - /// - /// - /// Thrown if this method is not called within a write lock - /// - public void NewContentTypesLocked(IEnumerable types) - { - EnsureLocked(); - - foreach (IPublishedContentType type in types) - { - SetContentTypeLocked(type); - } - } - - /// - /// Sets data for updated content types - /// - /// - /// - /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock - /// otherwise an exception will occur. - /// - /// - /// Thrown if this method is not called within a write lock - /// - public void UpdateContentTypesLocked(IEnumerable types) - { - // nothing to do if this is empty, no need to lock/allocate/iterate/etc... - if (!types.Any()) - { - return; - } - - EnsureLocked(); - - var index = types.ToDictionary(x => x.Id, x => x); - - foreach (IPublishedContentType type in index.Values) - { - SetContentTypeLocked(type); - } - - foreach (LinkedNode link in _contentNodes.Values) - { - ContentNode? node = link.Value; - if (node == null) - { - continue; - } - - var contentTypeId = node.ContentType.Id; - if (index.TryGetValue(contentTypeId, out IPublishedContentType? contentType) == false) - { - continue; - } - - SetValueLocked(_contentNodes, node.Id, new ContentNode(node, _publishedModelFactory, contentType)); - } - } - - /// - /// Updates/sets data for all content types - /// - /// - /// - /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock - /// otherwise an exception will occur. - /// - /// - /// Thrown if this method is not called within a write lock - /// - public void SetAllContentTypesLocked(IEnumerable? types) - { - EnsureLocked(); - - // clear all existing content types - ClearLocked(_contentTypesById); - ClearLocked(_contentTypesByAlias); - - if (types is not null) - { - // set all new content types - foreach (IPublishedContentType type in types) - { - SetContentTypeLocked(type); - } - } - - // beware! at that point the cache is inconsistent, - // assuming we are going to SetAll content items! - } - - /// - /// Updates/sets/removes data for content types - /// - /// - /// - /// - /// - /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock - /// otherwise an exception will occur. - /// - /// - /// Thrown if this method is not called within a write lock - /// - public void UpdateContentTypesLocked( - IReadOnlyCollection? removedIds, - IReadOnlyCollection refreshedTypes, - IReadOnlyCollection kits) - { - EnsureLocked(); - - IReadOnlyCollection removedIdsA = removedIds ?? Array.Empty(); - IReadOnlyCollection refreshedTypesA = - refreshedTypes ?? Array.Empty(); - var refreshedIdsA = refreshedTypesA.Select(x => x.Id).ToList(); - kits = kits ?? Array.Empty(); - - if (kits.Count == 0 && refreshedIdsA.Count == 0 && removedIdsA.Count == 0) - { - return; // exit - there is nothing to do here - } - - var removedContentTypeNodes = new List(); - var refreshedContentTypeNodes = new List(); - - // find all the nodes that are either refreshed or removed, - // because of their content type being either refreshed or removed - foreach (LinkedNode link in _contentNodes.Values) - { - ContentNode? node = link.Value; - if (node == null) - { - continue; - } - - var contentTypeId = node.ContentType.Id; - if (removedIdsA.Contains(contentTypeId)) - { - removedContentTypeNodes.Add(node.Id); - } - - if (refreshedIdsA.Contains(contentTypeId)) - { - refreshedContentTypeNodes.Add(node.Id); - } - } - - // perform deletion of content with removed content type - // removing content types should have removed their content already - // but just to be 100% sure, clear again here - foreach (var node in removedContentTypeNodes) - { - ClearBranchLocked(node); - } - - // perform deletion of removed content types - foreach (var id in removedIdsA) - { - if (_contentTypesById.TryGetValue(id, out LinkedNode? link) == false || - link.Value == null) - { - continue; - } - - SetValueLocked(_contentTypesById, id, null); - SetValueLocked(_contentTypesByAlias, link.Value.Alias, null); - } - - // perform update of refreshed content types - foreach (IPublishedContentType type in refreshedTypesA) - { - SetContentTypeLocked(type); - } - - // perform update of content with refreshed content type - from the kits - // skip missing type, skip missing parents & un-buildable kits - what else could we do? - // kits are ordered by level, so ParentExists is ok here - var visited = new List(); - foreach (ContentNodeKit kit in kits.Where(x => - refreshedIdsA.Contains(x.ContentTypeId) && - BuildKit(x, out _))) - { - // replacing the node: must preserve the relations - ContentNode? node = GetHead(_contentNodes, kit.Node.Id)?.Value; - if (node != null) - { - // Preserve children - kit.Node.FirstChildContentId = node.FirstChildContentId; - kit.Node.LastChildContentId = node.LastChildContentId; - - // Also preserve siblings - kit.Node.NextSiblingContentId = node.NextSiblingContentId; - kit.Node.PreviousSiblingContentId = node.PreviousSiblingContentId; - } - - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - - visited.Add(kit.Node.Id); - if (_localDb != null) - { - RegisterChange(kit.Node.Id, kit); - } - } - - // all content should have been refreshed - but... - IEnumerable orphans = refreshedContentTypeNodes.Except(visited); - foreach (var id in orphans) - { - ClearBranchLocked(id); - } - } - - /// - /// Updates data types - /// - /// - /// - /// - /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock - /// otherwise an exception will occur. - /// - /// - /// Thrown if this method is not called within a write lock - /// - public void UpdateDataTypesLocked(IEnumerable dataTypeIds, Func getContentType) - { - EnsureLocked(); - - IPublishedContentType[] contentTypes = _contentTypesById - .Where(kvp => - kvp.Value.Value != null && - kvp.Value.Value.PropertyTypes.Any(p => dataTypeIds.Contains(p.DataType.Id))) - .Select(kvp => kvp.Value.Value) - .Select(x => getContentType(x!.Id)) - .WhereNotNull() // poof, gone, very unlikely and probably an anomaly - .ToArray(); - - // all content types that are affected by this data type update must be updated - foreach (IPublishedContentType contentType in contentTypes) - { - SetContentTypeLocked(contentType); - } - - var contentTypeIdsA = contentTypes.Select(x => x.Id).ToArray(); - var contentTypeNodes = new Dictionary>(); - foreach (var id in contentTypeIdsA) - { - contentTypeNodes[id] = new List(); - } - - foreach (LinkedNode link in _contentNodes.Values) - { - ContentNode? node = link.Value; - if (node != null && contentTypeIdsA.Contains(node.ContentType.Id)) - { - contentTypeNodes[node.ContentType.Id].Add(node.Id); - } - } - - foreach (IPublishedContentType contentType in contentTypes) - { - // again, weird situation - if (contentTypeNodes.ContainsKey(contentType.Id) == false) - { - continue; - } - - foreach (var id in contentTypeNodes[contentType.Id]) - { - _contentNodes.TryGetValue(id, out LinkedNode? link); - if (link?.Value == null) - { - continue; - } - - var node = new ContentNode(link.Value, _publishedModelFactory, contentType); - SetValueLocked(_contentNodes, id, node); - if (_localDb != null) - { - RegisterChange(id, node.ToKit()); - } - } - } - } - - /// - /// Validate the and try to create a parent - /// - /// - /// - /// - /// Returns false if the parent was not found or if the kit validation failed - /// - private bool BuildKit(ContentNodeKit kit, [MaybeNullWhen(false)] out LinkedNode parent) - { - // make sure parent exists - parent = GetParentLink(kit.Node, null); - if (parent == null) - { - _logger.LogWarning("Skip item id={kitNodeId}, could not find parent id={kitNodeParentContentId}.", kit.Node.Id, kit.Node.ParentContentId); - return false; - } - - // We cannot continue if there's no value. This shouldn't happen but it can happen if the database umbracoNode.path - // data is invalid/corrupt. If that is the case, the parentId might be ok but not the Path which can result in null - // because the data sort operation is by path. - if (parent.Value == null) - { - _logger.LogWarning( - "Skip item id={kitNodeId}, no Data assigned for linked node with path {kitNodePath} and parent id {kitNodeParentContentId}. This can indicate data corruption for the Path value for node {kitNodeId}. See the Health Check dashboard in Settings to resolve data integrity issues.", - kit.Node.Id, - kit.Node.Path, - kit.Node.ParentContentId, - kit.Node.Id); - return false; - } - - // make sure the kit is valid - if (kit.DraftData == null && kit.PublishedData == null) - { - _logger.LogWarning("Skip item id={kitNodeId}, both draft and published data are null.", kit.Node.Id); - return false; - } - - // unknown = bad - if (_contentTypesById.TryGetValue(kit.ContentTypeId, out LinkedNode? link) == false || - link.Value == null) - { - _logger.LogWarning("Skip item id={kitNodeId}, could not find content type id={kitContentTypeId}.", kit.Node.Id, kit.ContentTypeId); - return false; - } - - // check whether parent is published - var canBePublished = ParentPublishedLocked(kit); - - // and use - kit.Build(link.Value, _publishedSnapshotAccessor, _variationContextAccessor, _publishedModelFactory, canBePublished); - - return true; - } - - #endregion - - #region Set, Clear, Get - - public int Count => _contentNodes.Count; - - /// - /// Get the most recent version of the LinkedNode stored in the dictionary for the supplied key - /// - /// - /// - /// - /// - /// - private static LinkedNode? GetHead( - ConcurrentDictionary> dict, - TKey key) - where TValue : class? - where TKey : notnull - { - dict.TryGetValue(key, out LinkedNode? link); // else null - return link; - } - - /// - /// Sets the data for a - /// - /// - /// - /// - /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock - /// otherwise an exception will occur. - /// - /// - /// Thrown if this method is not called within a write lock - /// - public bool SetLocked(ContentNodeKit kit) - { - EnsureLocked(); - - // ReSharper disable LocalizableElement - if (kit.IsEmpty) - { - throw new ArgumentException("Kit is empty.", nameof(kit)); - } - - if (kit.Node.FirstChildContentId > 0) - { - throw new ArgumentException("Kit content cannot have children.", nameof(kit)); - } - - // ReSharper restore LocalizableElement - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Set content ID: {KitNodeId}", kit.Node.Id); - } - - // get existing - _contentNodes.TryGetValue(kit.Node.Id, out LinkedNode? link); - ContentNode? existing = link?.Value; - - if (!BuildKit(kit, out LinkedNode? parent)) - { - return false; - } - - // moving? - var moving = existing != null && existing.ParentContentId != kit.Node.ParentContentId; - - // manage children - if (existing != null) - { - kit.Node.FirstChildContentId = existing.FirstChildContentId; - kit.Node.LastChildContentId = existing.LastChildContentId; - } - - // set - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - if (_localDb != null) - { - RegisterChange(kit.Node.Id, kit); - } - - // manage the tree - if (existing == null) - { - // new, add to parent - AddTreeNodeLocked(kit.Node, parent); - } - else if (moving || existing.SortOrder != kit.Node.SortOrder) - { - // moved, remove existing from its parent, add content to its parent - RemoveTreeNodeLocked(existing); - AddTreeNodeLocked(kit.Node); - } - else - { - // replacing existing, handle siblings - kit.Node.NextSiblingContentId = existing.NextSiblingContentId; - kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId; - } - - _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; - - return true; - } - - private void ClearRootLocked() - { - if (_root.Gen != _liveGen) - { - _root = new LinkedNode(new ContentNode(), _liveGen, _root); - } - else if (_root.Value is not null) - { - _root.Value.FirstChildContentId = -1; - } - } - - /// - /// Builds all kits on startup using a fast forward only cursor - /// - /// - /// All kits sorted by Level + Parent Id + Sort order - /// - /// True if the data is coming from the database (not the local cache db) - /// - /// - /// - /// This requires that the collection is sorted by Level + ParentId + Sort Order. - /// This should be used only on a site startup as the first generations. - /// This CANNOT be used after startup since it bypasses all checks for Generations. - /// - /// - /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock - /// otherwise an exception will occur. - /// - /// - /// - /// Thrown if this method is not called within a write lock - /// - [Obsolete("Use the overload that takes a 'kitGroupSize' parameter instead")] - public bool SetAllFastSortedLocked(IEnumerable kits, bool fromDb) => - SetAllFastSortedLocked(kits, 1, fromDb); - - /// - /// Builds all kits on startup using a fast forward only cursor - /// - /// - /// All kits sorted by Level + Parent Id + Sort order - /// - /// - /// True if the data is coming from the database (not the local cache db) - /// - /// - /// - /// This requires that the collection is sorted by Level + ParentId + Sort Order. - /// This should be used only on a site startup as the first generations. - /// This CANNOT be used after startup since it bypasses all checks for Generations. - /// - /// - /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock - /// otherwise an exception will occur. - /// - /// - /// - /// Thrown if this method is not called within a write lock - /// - public bool SetAllFastSortedLocked(IEnumerable kits, int kitGroupSize, bool fromDb) - { - EnsureLocked(); - - var ok = true; - - ClearLocked(_contentNodes); - ClearRootLocked(); - - // The name of the game here is to populate each kit's - // FirstChildContentId - // LastChildContentId - // NextSiblingContentId - // PreviousSiblingContentId - ContentNode? previousNode = null; - ContentNode? parent = null; - - // By using InGroupsOf() here we are forcing the database query result extraction to retrieve items in batches, - // reducing the possibility of a database timeout (ThreadAbortException) on large datasets. - // This in turn reduces the possibility that the NuCache file will remain locked, because an exception - // here results in the calling method to not release the lock. - - // However the larger the batck size, the more content loaded into memory. So by default, this is set to 1 and can be increased by setting - // the configuration setting Umbraco:CMS:NuCache:KitPageSize to a higher value. - - // If we are not loading from the database, then we can ignore this restriction. - foreach (IEnumerable kitGroup in - kits.InGroupsOf(!fromDb || kitGroupSize < 1 ? 1 : kitGroupSize)) - { - foreach (ContentNodeKit kit in kitGroup) - { - if (!BuildKit(kit, out LinkedNode? parentLink)) - { - ok = false; - continue; // skip that one - } - - ContentNode thisNode = kit.Node; - - if (parent == null) - { - // first parent - parent = parentLink.Value; - parent!.FirstChildContentId = thisNode.Id; // this node is the first node - } - else if (parent.Id != parentLink.Value!.Id) - { - // new parent - parent = parentLink.Value; - parent.FirstChildContentId = thisNode.Id; // this node is the first node - previousNode = null; // there is no previous sibling - } - - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Set {thisNodeId} with parent {thisNodeParentContentId}", thisNode.Id, thisNode.ParentContentId); - } - - SetValueLocked(_contentNodes, thisNode.Id, thisNode); - - // if we are initializing from the database source ensure the local db is updated - if (fromDb && _localDb != null) - { - RegisterChange(thisNode.Id, kit); - } - - // this node is always the last child - parent.LastChildContentId = thisNode.Id; - - // wire previous node as previous sibling - if (previousNode != null) - { - previousNode.NextSiblingContentId = thisNode.Id; - thisNode.PreviousSiblingContentId = previousNode.Id; - } - - // this node becomes the previous node - previousNode = thisNode; - - _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; - } - } - - return ok; - } - - /// - /// Set all data for a collection of - /// - /// - /// - /// - /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock - /// otherwise an exception will occur. - /// - /// - /// Thrown if this method is not called within a write lock - /// - [Obsolete("Use the overload that takes the 'kitGroupSize' and 'fromDb' parameters instead")] - public bool SetAllLocked(IEnumerable kits) => SetAllLocked(kits, 1, false); - - /// - /// Set all data for a collection of - /// - /// - /// - /// True if the data is coming from the database (not the local cache db) - /// - /// - /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock - /// otherwise an exception will occur. - /// - /// - /// Thrown if this method is not called within a write lock - /// - public bool SetAllLocked(IEnumerable kits, int kitGroupSize, bool fromDb) - { - EnsureLocked(); - - var ok = true; - - ClearLocked(_contentNodes); - ClearRootLocked(); - - // do NOT clear types else they are gone! - // ClearLocked(_contentTypesById); - // ClearLocked(_contentTypesByAlias); - - // By using InGroupsOf() here we are forcing the database query result extraction to retrieve items in batches, - // reducing the possibility of a database timeout (ThreadAbortException) on large datasets. - // This in turn reduces the possibility that the NuCache file will remain locked, because an exception - // here results in the calling method to not release the lock. - - // However the larger the batck size, the more content loaded into memory. So by default, this is set to 1 and can be increased by setting - // the configuration setting Umbraco:CMS:NuCache:KitPageSize to a higher value. - - // If we are not loading from the database, then we can ignore this restriction. - foreach (IEnumerable kitGroup in - kits.InGroupsOf(!fromDb || kitGroupSize < 1 ? 1 : kitGroupSize)) - { - foreach (ContentNodeKit kit in kitGroup) - { - if (!BuildKit(kit, out LinkedNode? parent)) - { - ok = false; - continue; // skip that one - } - - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Set {kitNodeId} with parent {kitNodeParentContentId}", kit.Node.Id, kit.Node.ParentContentId); - } - - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - - if (_localDb != null) - { - RegisterChange(kit.Node.Id, kit); - } - - AddTreeNodeLocked(kit.Node, parent); - - _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; - } - } - - return ok; - } - - /// - /// Sets data for a branch of - /// - /// - /// - /// - /// - /// - /// IMPORTANT kits must be sorted out by LEVEL and by SORT ORDER - /// - /// - /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock - /// otherwise an exception will occur. - /// - /// - /// - /// Thrown if this method is not called within a write lock - /// - public bool SetBranchLocked(int rootContentId, IEnumerable kits) - { - EnsureLocked(); - - var ok = true; - - // get existing - _contentNodes.TryGetValue(rootContentId, out LinkedNode? link); - ContentNode? existing = link?.Value; - - // clear - if (existing != null) - { - // this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen - ClearBranchLocked(existing); - - // TODO: This removes the current GEN from the tree - do we really want to do that? (not sure if this is still an issue....) - RemoveTreeNodeLocked(existing); - } - - // now add them all back - foreach (ContentNodeKit kit in kits) - { - if (!BuildKit(kit, out LinkedNode? parent)) - { - ok = false; - continue; // skip that one - } - - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - if (_localDb != null) - { - RegisterChange(kit.Node.Id, kit); - } - - AddTreeNodeLocked(kit.Node, parent); - - _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; - } - - return ok; - } - - /// - /// Clears data for a given node id - /// - /// - /// - /// - /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock - /// otherwise an exception will occur. - /// - /// - /// Thrown if this method is not called within a write lock - /// - public bool ClearLocked(int id) - { - EnsureLocked(); - - // try to find the content - // if it is not there, nothing to do - _contentNodes.TryGetValue(id, out LinkedNode? link); // else null - if (link?.Value == null) - { - return false; - } - - ContentNode? content = link.Value; - - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Clear content ID: {ContentId}", content.Id); - } - - // clear the entire branch - ClearBranchLocked(content); - - // manage the tree - RemoveTreeNodeLocked(content); - - return true; - } - - private void ClearBranchLocked(int id) - { - _contentNodes.TryGetValue(id, out LinkedNode? link); - if (link?.Value == null) - { - return; - } - - ClearBranchLocked(link.Value); - } - - private void ClearBranchLocked(ContentNode? content) - { - // This should never be null, all code that calls this method is null checking but we've seen - // issues of null ref exceptions in issue reports so we'll double check here - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - SetValueLocked(_contentNodes, content.Id, null); - if (_localDb != null) - { - RegisterChange(content.Id, ContentNodeKit.Null); - } - - _contentKeyToIdMap.TryRemove(content.Uid, out _); - - var id = content.FirstChildContentId; - while (id > 0) - { - // get the required link node, this ensures that both `link` and `link.Value` are not null - LinkedNode link = GetRequiredLinkedNode(id, "child", null); - ContentNode? linkValue = link.Value; // capture local since clearing in recurse can clear it - ClearBranchLocked(linkValue); // recurse - id = linkValue?.NextSiblingContentId ?? 0; - } - } - - /// - /// Gets the link node and if it doesn't exist throw a - /// - /// - /// - /// the generation requested, null for the latest stored - /// - private LinkedNode GetRequiredLinkedNode(int id, string description, long? gen) - { - if (_contentNodes.TryGetValue(id, out LinkedNode? link)) - { - link = GetLinkedNodeGen(link, gen); - if (link is not null && link.Value is not null) - { - return link!; - } - } - - throw new PanicException($"failed to get {description} with id={id}"); - } - - /// - /// Gets the parent link node, may be null or root if ParentContentId is less than 0 - /// - /// the generation requested, null for the latest stored - /// The content node - private LinkedNode? GetParentLink(ContentNode content, long? gen) - { - if (content.ParentContentId < 0) - { - LinkedNode? root = GetLinkedNodeGen(_root, gen); - return root; - } - - if (_contentNodes.TryGetValue(content.ParentContentId, out LinkedNode? link)) - { - link = GetLinkedNodeGen(link, gen); - } - - if (link is not null && link.Value is not null) - { - return link!; - } - - return null; - } - - /// - /// Gets the linked parent node and if it doesn't exist throw a - /// - /// The content node - /// the generation requested, null for the latest stored - /// - private LinkedNode GetRequiredParentLink(ContentNode content, long? gen) => content.ParentContentId < 0 - ? _root - : GetRequiredLinkedNode(content.ParentContentId, "parent", gen); - - /// - /// Iterates over the LinkedNode's generations to find the correct one - /// - /// - /// The generation requested, use null to avoid the lookup - /// - private LinkedNode? GetLinkedNodeGen(LinkedNode? link, long? gen) - where TValue : class? - { - if (!gen.HasValue) - { - return link; - } - - // find the correct snapshot, find the first that is <= the requested gen - while (link != null && link.Gen > gen) - { - link = link.Next; - } - - return link; - } - - /// - /// This removes this current node from the tree hiearchy by removing it from it's parent's linked list - /// - /// - /// - /// This is called within a lock which means a new Gen is being created therefore this will not modify any existing - /// content in a Gen. - /// - private void RemoveTreeNodeLocked(ContentNode content) - { - // NOTE: DO NOT modify `content` here, this would modify data for an existing Gen, all modifications are done to clones - // which would be targeting the new Gen. - LinkedNode parentLink = content.ParentContentId < 0 - ? _root - : GetRequiredLinkedNode(content.ParentContentId, "parent", null); - - ContentNode? parent = parentLink.Value; - - // must have children - if (parent!.FirstChildContentId < 0) - { - throw new PanicException("no children"); - } - - // if first/last, clone parent, then remove - if (parent.FirstChildContentId == content.Id || parent.LastChildContentId == content.Id) - { - parent = GenCloneLocked(parentLink); - } - - if (parent is not null) - { - if (parent.FirstChildContentId == content.Id) - { - parent.FirstChildContentId = content.NextSiblingContentId; - } - - if (parent.LastChildContentId == content.Id) - { - parent.LastChildContentId = content.PreviousSiblingContentId; - } - } - - // maintain linked list - if (content.NextSiblingContentId > 0) - { - LinkedNode nextLink = - GetRequiredLinkedNode(content.NextSiblingContentId, "next sibling", null); - ContentNode? next = GenCloneLocked(nextLink); - - if (next is not null) - { - next.PreviousSiblingContentId = content.PreviousSiblingContentId; - } - } - - if (content.PreviousSiblingContentId > 0) - { - LinkedNode prevLink = - GetRequiredLinkedNode(content.PreviousSiblingContentId, "previous sibling", null); - ContentNode? prev = GenCloneLocked(prevLink); - - if (prev is not null) - { - prev.NextSiblingContentId = content.NextSiblingContentId; - } - } - } - - private bool ParentPublishedLocked(ContentNodeKit kit) - { - if (kit.Node.ParentContentId < 0) - { - return true; - } - - LinkedNode? link = GetParentLink(kit.Node, null); - ContentNode? node = link?.Value; - return node != null && node.HasPublished; - } - - private ContentNode? GenCloneLocked(LinkedNode link) - { - ContentNode? node = link.Value; - - if (node != null && link.Gen != _liveGen) - { - node = new ContentNode(node, _publishedModelFactory); - if (link == _root) - { - SetRootLocked(node); - } - else - { - SetValueLocked(_contentNodes, node.Id, node); - } - } - - return node; - } - - /// - /// Adds a node to the tree structure. - /// - private void AddTreeNodeLocked(ContentNode content, LinkedNode? parentLink = null) - { - parentLink = parentLink ?? GetRequiredParentLink(content, null); - - ContentNode? parent = parentLink.Value; - - // We are doing a null check here but this should no longer be possible because we have a null check in BuildKit - // for the parent.Value property and we'll output a warning. However I'll leave this additional null check in place. - // see https://github.com/umbraco/Umbraco-CMS/issues/7868 - if (parent == null) - { - throw new PanicException( - $"A null Value was returned on the {nameof(parentLink)} LinkedNode with id={content.ParentContentId}, potentially your database paths are corrupted."); - } - - // if parent has no children, clone parent + add as first child - if (parent.FirstChildContentId < 0) - { - parent = GenCloneLocked(parentLink); - if (parent is not null) - { - parent.FirstChildContentId = content.Id; - parent.LastChildContentId = content.Id; - } - - return; - } - - // get parent's first child - LinkedNode childLink = GetRequiredLinkedNode(parent.FirstChildContentId, "first child", null); - ContentNode? child = childLink.Value; - - // if first, clone parent + insert as first child - // NOTE: Don't perform this check if loading from local DB since we know it's already sorted - if (child?.SortOrder > content.SortOrder) - { - content.NextSiblingContentId = parent.FirstChildContentId; - content.PreviousSiblingContentId = -1; - - parent = GenCloneLocked(parentLink); - if (parent is not null) - { - parent.FirstChildContentId = content.Id; - } - - child = GenCloneLocked(childLink); - - if (child is not null) - { - child.PreviousSiblingContentId = content.Id; - } - - return; - } - - // get parent's last child - LinkedNode lastChildLink = GetRequiredLinkedNode(parent.LastChildContentId, "last child", null); - ContentNode? lastChild = lastChildLink.Value; - - // if last, clone parent + append as last child - if (lastChild?.SortOrder <= content.SortOrder) - { - content.PreviousSiblingContentId = parent.LastChildContentId; - content.NextSiblingContentId = -1; - - parent = GenCloneLocked(parentLink); - if (parent is not null) - { - parent.LastChildContentId = content.Id; - } - - lastChild = GenCloneLocked(lastChildLink); - - if (lastChild is not null) - { - lastChild.NextSiblingContentId = content.Id; - } - - return; - } - - // else it's going somewhere in the middle, - // TODO: There was a note about performance when this occurs and that this only happens when moving and not very often, but that is not true, - // this also happens anytime a middle node is unpublished or republished (which causes a branch update), i'm unsure if this has perf impacts, - // i think this used to but it doesn't seem bad anymore that I can see... - while (child?.NextSiblingContentId > 0) - { - // get next child - LinkedNode nextChildLink = - GetRequiredLinkedNode(child.NextSiblingContentId, "next child", null); - ContentNode? nextChild = nextChildLink.Value; - - // if here, clone previous + append/insert - // NOTE: Don't perform this check if loading from local DB since we know it's already sorted - if (nextChild?.SortOrder > content.SortOrder) - { - content.NextSiblingContentId = nextChild.Id; - content.PreviousSiblingContentId = nextChild.PreviousSiblingContentId; - - child = GenCloneLocked(childLink); - - if (child is not null) - { - child.NextSiblingContentId = content.Id; - } - - ContentNode? nnext = GenCloneLocked(nextChildLink); - - if (nnext is not null) - { - nnext.PreviousSiblingContentId = content.Id; - } - - return; - } - - childLink = nextChildLink; - child = nextChild; - } - - // should never get here - throw new PanicException("No more children."); - } - - // replaces the root node - private void SetRootLocked(ContentNode node) - { - if (_root.Gen != _liveGen) - { - _root = new LinkedNode(node, _liveGen, _root); - } - else - { - _root.Value = node; - } - } - - private void SetContentTypeLocked(IPublishedContentType type) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - - // ensure the key/id map is accurate - _contentTypeKeyToIdMap[type.Key] = type.Id; - } - - // set a node (just the node, not the tree) - private void SetValueLocked(ConcurrentDictionary> dict, TKey key, TValue value) - where TValue : class? - where TKey : notnull - { - // this is safe only because we're write-locked - LinkedNode? link = GetHead(dict, key); - if (link != null) - { - // already in the dict - if (link.Gen != _liveGen) - { - // for an older gen - if value is different then insert a new - // link for the new gen, with the new value - if (link.Value != value) - { - dict.TryUpdate(key, new LinkedNode(value, _liveGen, link), link); - } - } - else - { - // for the live gen - we can fix the live gen - and remove it - // if value is null and there's no next gen - if (value == null && link.Next == null) - { - dict.TryRemove(key, out link); - } - else - { - link.Value = value; - } - } - } - else - { - dict.TryAdd(key, new LinkedNode(value, _liveGen)); - } - } - - private void ClearLocked(ConcurrentDictionary> dict) - where TValue : class? - where TKey : notnull - { - // this is safe only because we're write-locked - foreach (KeyValuePair> kvp in dict.Where(x => x.Value != null)) - { - if (kvp.Value.Gen != _liveGen) - { - var link = new LinkedNode(null, _liveGen, kvp.Value); - dict.TryUpdate(kvp.Key, link, kvp.Value); - } - else - { - kvp.Value.Value = null; - } - } - } - - public ContentNode? Get(int id, long gen) => GetValue(_contentNodes, id, gen); - - public ContentNode? Get(Guid uid, long gen) => - _contentKeyToIdMap.TryGetValue(uid, out var id) - ? GetValue(_contentNodes, id, gen) - : null; - - public IEnumerable GetAtRoot(long gen) - { - LinkedNode? root = GetLinkedNodeGen(_root, gen); - if (root == null) - { - yield break; - } - - var id = root.Value?.FirstChildContentId; - - while (id > 0) - { - LinkedNode link = GetRequiredLinkedNode(id.Value, "root", gen); - yield return link.Value; - id = link.Value?.NextSiblingContentId; - } - } - - private TValue? GetValue(ConcurrentDictionary> dict, TKey key, long gen) - where TValue : class? - where TKey : notnull - { - // look ma, no lock! - LinkedNode? link = GetHead(dict, key); - link = GetLinkedNodeGen(link, gen); - return link?.Value; // may be null - } - - public IEnumerable GetAll(long gen) - { - // enumerating on .Values locks the concurrent dictionary, - // so better get a shallow clone in an array and release - LinkedNode[] links = _contentNodes.Values.ToArray(); - foreach (LinkedNode l in links) - { - LinkedNode? link = GetLinkedNodeGen(l, gen); - if (link?.Value != null) - { - yield return link.Value; - } - } - } - - public bool IsEmpty(long gen) - { - var has = _contentNodes.Any(x => - { - LinkedNode? link = GetLinkedNodeGen(x.Value, gen); - return link?.Value != null; - }); - return has == false; - } - - public IPublishedContentType? GetContentType(int id, long gen) => GetValue(_contentTypesById, id, gen); - - public IPublishedContentType? GetContentType(string alias, long gen) => GetValue(_contentTypesByAlias, alias, gen); - - public IPublishedContentType? GetContentType(Guid key, long gen) - { - if (!_contentTypeKeyToIdMap.TryGetValue(key, out var id)) - { - return null; - } - - return GetContentType(id, gen); - } - - #endregion - - #region Snapshots - - public Snapshot CreateSnapshot() - { - lock (_rlocko) - { - // if no next generation is required, and we already have one, - // use it and create a new snapshot - if (_nextGen == false && _genObj != null) - { - return new Snapshot(this, _genObj.GetGenRef() -#if DEBUG - , _loggerFactory.CreateLogger() -#endif - ); - } - - // else we need to try to create a new gen ref - // whether we are wlocked or not, noone can rlock while we do, - // so _liveGen and _nextGen are safe - if (Monitor.IsEntered(_wlocko)) - { - // write-locked, cannot use latest gen (at least 1) so use previous - var snapGen = _nextGen ? _liveGen - 1 : _liveGen; - - // create a new gen ref unless we already have it - if (_genObj == null) - { - _genObjs.Enqueue(_genObj = new GenObj(snapGen)); - } - else if (_genObj.Gen != snapGen) - { - throw new PanicException( - $"The generation {_genObj.Gen} does not equal the snapshot generation {snapGen}"); - } - } - else - { - // not write-locked, can use latest gen, create a new gen ref - _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); - _nextGen = false; // this is the ONLY thing that triggers a _liveGen++ - } - - // so... - // the genRefRef has a weak ref to the genRef, and is queued - // the snapshot has a ref to the genRef, which has a ref to the genRefRef - // when the snapshot is disposed, it decreases genRefRef counter - // so after a while, one of these conditions is going to be true: - // - the genRefRef counter is zero because all snapshots have properly been disposed - // - the genRefRef weak ref is dead because all snapshots have been collected - // in both cases, we will dequeue and collect - var snapshot = new Snapshot(this, _genObj.GetGenRef() -#if DEBUG - , _loggerFactory.CreateLogger() -#endif - ); - - // reading _floorGen is safe if _collectTask is null - if (_collectTask == null && _collectAuto && _liveGen - _floorGen > CollectMinGenDelta) - { - CollectAsyncLocked(); - } - - return snapshot; - } - } - - public Snapshot LiveSnapshot => new(this, _liveGen -#if DEBUG - , _loggerFactory.CreateLogger() -#endif - ); - - public Task CollectAsync() - { - lock (_rlocko) - { - return CollectAsyncLocked(); - } - } - - private Task CollectAsyncLocked() - { - // NOTE: What in the heck is going on here? Why is any of this running in async contexts? - // SD: From what I can tell this was designed to be a set and forget background task to do the - // collecting which is why it's called from non-async methods within this class. This is - // slightly dangerous because it's not taking into account app shutdown. - // TODO: There should be a different method or class responsible for executing the cleanup on a - // background (set and forget) thread. - if (_collectTask != null) - { - return _collectTask; - } - - // ReSharper disable InconsistentlySynchronizedField - Task task = _collectTask = Task.Run(Collect); - _collectTask.ContinueWith( - _ => - { - lock (_rlocko) - { - _collectTask = null; - } - }, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default); - - // ReSharper restore InconsistentlySynchronizedField - return task; - } - - private void Collect() - { - // see notes in CreateSnapshot -#if DEBUG - _logger.LogDebug("Collect."); -#endif - while (_genObjs.TryPeek(out GenObj? genObj) && (genObj.Count == 0 || genObj.WeakGenRef.IsAlive == false)) - { - _genObjs.TryDequeue(out genObj); // cannot fail since TryPeek has succeeded - _floorGen = genObj!.Gen; -#if DEBUG - // _logger.LogDebug("_floorGen=" + _floorGen + ", _liveGen=" + _liveGen); -#endif - } - - Collect(_contentNodes); - CollectRoot(); - Collect(_contentTypesById); - Collect(_contentTypesByAlias); - } - - private void CollectRoot() - { - LinkedNode? link = _root; - while (link.Next != null && link.Next.Gen > _floorGen) - { - link = link.Next; - } - - link.Next = null; - } - - private void Collect(ConcurrentDictionary> dict) - where TValue : class? - where TKey : notnull - { - // it is OK to enumerate a concurrent dictionary and it does not lock - // it - and here it's not an issue if we skip some items, they will be - // processed next time we collect - long liveGen; - - // r is good - lock (_rlocko) - { - liveGen = _liveGen; - if (_nextGen == false) - { - liveGen += 1; - } - } - - foreach (KeyValuePair> kvp in dict) - { - LinkedNode? link = kvp.Value; - -#if DEBUG - // _logger.LogDebug("Collect id:" + kvp.Key + ", gen:" + link.Gen + - // ", nxt:" + (link.Next == null ? "null" : "link") + - // ", val:" + (link.Value == null ? "null" : "value")); -#endif - - // reasons to collect the head: - // gen must be < liveGen (we never collect live gen) - // next == null && value == null (we have no data at all) - // next != null && value == null BUT gen > floor (noone wants us) - // not live means .Next and .Value are safe - if (link.Gen < liveGen && link.Value == null - && (link.Next == null || link.Gen <= _floorGen)) - { - // not live, null value, no next link = remove that one -- but only if - // the dict has not been updated, have to do it via ICollection<> (thanks - // Mr Toub) -- and if the dict has been updated there is nothing to collect - var idict = dict as ICollection>>; - idict.Remove(kvp); - continue; - } - - // in any other case we're not collecting the head, we need to go to Next - // and if there is no Next, skip - if (link.Next == null) - { - continue; - } - - // else go to Next and loop while above floor, and kill everything below - while (link.Next != null && link.Next.Gen > _floorGen) - { - link = link.Next; - } - - link.Next = null; - } - } - - // TODO: This is never used? Should it be? Maybe move to TestHelper below? - // public async Task WaitForPendingCollect() - // { - // Task task; - // lock (_rlocko) - // { - // task = _collectTask; - // } - // if (task != null) - // await task; - // } - public long GenCount => _genObjs.Count; - - public long SnapCount => _genObjs.Sum(x => x.Count); - - #endregion - - #region Internals/Unit testing - - private TestHelper? _unitTesting; - - // note: nothing here is thread-safe - internal class TestHelper - { - private readonly ContentStore _store; - - public TestHelper(ContentStore store) => _store = store; - - public long LiveGen => _store._liveGen; - - public long FloorGen => _store._floorGen; - - public bool NextGen => _store._nextGen; - - public bool CollectAuto - { - get => _store._collectAuto; - set => _store._collectAuto = value; - } - - /// - /// Return a list of Gen/ContentNode values - /// - /// - /// - public (long gen, ContentNode? contentNode)[] GetValues(int id) - { - _store._contentNodes.TryGetValue(id, out LinkedNode? link); // else null - - if (link == null) - { - return Array.Empty<(long, ContentNode?)>(); - } - - var tuples = new List<(long, ContentNode?)>(); - do - { - tuples.Add((link.Gen, link.Value)); - link = link.Next; - } - while (link != null); - - return tuples.ToArray(); - } - } - - internal TestHelper Test => _unitTesting ?? (_unitTesting = new TestHelper(this)); - - #endregion -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs deleted file mode 100644 index f81ff21a07..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs +++ /dev/null @@ -1,53 +0,0 @@ -using CSharpTest.Net.Serialization; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -/// -/// Serializes/Deserializes data to BTree data source for -/// -public class ContentDataSerializer : ISerializer -{ - private static readonly DictionaryOfPropertyDataSerializer S_defaultPropertiesSerializer = new(); - private static readonly DictionaryOfCultureVariationSerializer S_defaultCultureVariationsSerializer = new(); - private readonly IDictionaryOfPropertyDataSerializer? _dictionaryOfPropertyDataSerializer; - - public ContentDataSerializer(IDictionaryOfPropertyDataSerializer? dictionaryOfPropertyDataSerializer = null) - { - _dictionaryOfPropertyDataSerializer = dictionaryOfPropertyDataSerializer; - if (_dictionaryOfPropertyDataSerializer == null) - { - _dictionaryOfPropertyDataSerializer = S_defaultPropertiesSerializer; - } - } - - public ContentData ReadFrom(Stream stream) - { - var published = PrimitiveSerializer.Boolean.ReadFrom(stream); - var name = PrimitiveSerializer.String.ReadFrom(stream); - var urlSegment = PrimitiveSerializer.String.ReadFrom(stream); - var versionId = PrimitiveSerializer.Int32.ReadFrom(stream); - DateTime versionDate = PrimitiveSerializer.DateTime.ReadFrom(stream); - var writerId = PrimitiveSerializer.Int32.ReadFrom(stream); - var templateId = PrimitiveSerializer.Int32.ReadFrom(stream); - IDictionary? - properties = - _dictionaryOfPropertyDataSerializer?.ReadFrom(stream); // TODO: We don't want to allocate empty arrays - IReadOnlyDictionary cultureInfos = - S_defaultCultureVariationsSerializer.ReadFrom(stream); // TODO: We don't want to allocate empty arrays - var cachedTemplateId = templateId == 0 ? (int?)null : templateId; - return new ContentData(name, urlSegment, versionId, versionDate, writerId, cachedTemplateId, published, properties, cultureInfos); - } - - public void WriteTo(ContentData value, Stream stream) - { - PrimitiveSerializer.Boolean.WriteTo(value.Published, stream); - PrimitiveSerializer.String.WriteTo(value.Name, stream); - PrimitiveSerializer.String.WriteTo(value.UrlSegment, stream); - PrimitiveSerializer.Int32.WriteTo(value.VersionId, stream); - PrimitiveSerializer.DateTime.WriteTo(value.VersionDate, stream); - PrimitiveSerializer.Int32.WriteTo(value.WriterId, stream); - PrimitiveSerializer.Int32.WriteTo(value.TemplateId ?? 0, stream); - _dictionaryOfPropertyDataSerializer?.WriteTo(value.Properties, stream); - S_defaultCultureVariationsSerializer.WriteTo(value.CultureInfos, stream); - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentNodeKitSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentNodeKitSerializer.cs deleted file mode 100644 index fbba6a2b17..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentNodeKitSerializer.cs +++ /dev/null @@ -1,83 +0,0 @@ -using CSharpTest.Net.Serialization; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -internal class ContentNodeKitSerializer : ISerializer -{ - private static readonly ContentDataSerializer S_defaultDataSerializer = new(); - private readonly ContentDataSerializer? _contentDataSerializer; - - public ContentNodeKitSerializer(ContentDataSerializer? contentDataSerializer = null) - { - _contentDataSerializer = contentDataSerializer; - if (_contentDataSerializer == null) - { - _contentDataSerializer = S_defaultDataSerializer; - } - } - - // static readonly ListOfIntSerializer ChildContentIdsSerializer = new ListOfIntSerializer(); - public ContentNodeKit ReadFrom(Stream stream) - { - var contentNode = new ContentNode( - PrimitiveSerializer.Int32.ReadFrom(stream), // id - PrimitiveSerializer.Guid.ReadFrom(stream), // uid - PrimitiveSerializer.Int32.ReadFrom(stream), // level - PrimitiveSerializer.String.ReadFrom(stream), // path - PrimitiveSerializer.Int32.ReadFrom(stream), // sort order - PrimitiveSerializer.Int32.ReadFrom(stream), // parent id - PrimitiveSerializer.DateTime.ReadFrom(stream), // date created - PrimitiveSerializer.Int32.ReadFrom(stream)); // creator id - - var contentTypeId = PrimitiveSerializer.Int32.ReadFrom(stream); - var hasDraft = PrimitiveSerializer.Boolean.ReadFrom(stream); - ContentData? draftData = null; - ContentData? publishedData = null; - if (hasDraft) - { - draftData = _contentDataSerializer?.ReadFrom(stream); - } - - var hasPublished = PrimitiveSerializer.Boolean.ReadFrom(stream); - if (hasPublished) - { - publishedData = _contentDataSerializer?.ReadFrom(stream); - } - - var kit = new ContentNodeKit( - contentNode, - contentTypeId, - draftData, - publishedData); - - return kit; - } - - public void WriteTo(ContentNodeKit value, Stream stream) - { - if (value.Node is not null) - { - PrimitiveSerializer.Int32.WriteTo(value.Node.Id, stream); - PrimitiveSerializer.Guid.WriteTo(value.Node.Uid, stream); - PrimitiveSerializer.Int32.WriteTo(value.Node.Level, stream); - PrimitiveSerializer.String.WriteTo(value.Node.Path, stream); - PrimitiveSerializer.Int32.WriteTo(value.Node.SortOrder, stream); - PrimitiveSerializer.Int32.WriteTo(value.Node.ParentContentId, stream); - PrimitiveSerializer.DateTime.WriteTo(value.Node.CreateDate, stream); - PrimitiveSerializer.Int32.WriteTo(value.Node.CreatorId, stream); - PrimitiveSerializer.Int32.WriteTo(value.ContentTypeId, stream); - } - - PrimitiveSerializer.Boolean.WriteTo(value.DraftData != null, stream); - if (value.DraftData != null) - { - _contentDataSerializer?.WriteTo(value.DraftData, stream); - } - - PrimitiveSerializer.Boolean.WriteTo(value.PublishedData != null, stream); - if (value.PublishedData != null) - { - _contentDataSerializer?.WriteTo(value.PublishedData, stream); - } - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs deleted file mode 100644 index 28f0670a5d..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs +++ /dev/null @@ -1,59 +0,0 @@ -using CSharpTest.Net.Serialization; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -/// -/// Serializes/Deserializes culture variant data as a dictionary for BTree -/// -internal class DictionaryOfCultureVariationSerializer : SerializerBase, - ISerializer> -{ - private static readonly IReadOnlyDictionary Empty = - new Dictionary(); - - public IReadOnlyDictionary ReadFrom(Stream stream) - { - // read variations count - var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); - if (pcount == 0) - { - return Empty; - } - - // read each variation - var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - for (var i = 0; i < pcount; i++) - { - var languageId = string.Intern(PrimitiveSerializer.String.ReadFrom(stream)); - var cultureVariation = new CultureVariation - { - Name = ReadStringObject(stream), - UrlSegment = ReadStringObject(stream), - Date = ReadDateTime(stream), - }; - dict[languageId] = cultureVariation; - } - - return dict; - } - - public void WriteTo(IReadOnlyDictionary? value, Stream stream) - { - IReadOnlyDictionary variations = value ?? Empty; - - // write variations count - PrimitiveSerializer.Int32.WriteTo(variations.Count, stream); - - // write each variation - foreach ((var culture, CultureVariation variation) in variations) - { - // TODO: it's weird we're dealing with cultures here, and languageId in properties - PrimitiveSerializer.String.WriteTo(culture, stream); // should never be null - WriteObject(variation.Name, stream); // write an object in case it's null (though... should not happen) - WriteObject( - variation.UrlSegment, - stream); // write an object in case it's null (though... should not happen) - PrimitiveSerializer.DateTime.WriteTo(variation.Date, stream); - } - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs deleted file mode 100644 index d820a3f4a2..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ /dev/null @@ -1,84 +0,0 @@ -using CSharpTest.Net.Serialization; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -/// -/// Serializes/Deserializes property data as a dictionary for BTree -/// -internal class DictionaryOfPropertyDataSerializer : SerializerBase, ISerializer>, - IDictionaryOfPropertyDataSerializer -{ - private static readonly PropertyData[] Empty = Array.Empty(); - - public IDictionary ReadFrom(Stream stream) - { - // read properties count - var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); - var dict = new Dictionary(pcount, StringComparer.InvariantCultureIgnoreCase); - - // read each property - for (var i = 0; i < pcount; i++) - { - // read property alias - var key = string.Intern(PrimitiveSerializer.String.ReadFrom(stream)); - - // read values count - var vcount = PrimitiveSerializer.Int32.ReadFrom(stream); - if (vcount == 0) - { - dict[key] = Empty; - continue; - } - - // create pdata and add to the dictionary - var pdatas = new PropertyData[vcount]; - - // for each value, read and add to pdata - for (var j = 0; j < vcount; j++) - { - var pdata = new PropertyData(); - pdatas[j] = pdata; - - // everything that can be null is read/written as object - // even though - culture and segment should never be null here, as 'null' represents - // the 'current' value, and string.Empty should be used to represent the invariant or - // neutral values - PropertyData throws when getting nulls, so falling back to - // string.Empty here - what else? - pdata.Culture = ReadStringObject(stream, true) ?? string.Empty; - pdata.Segment = ReadStringObject(stream, true) ?? string.Empty; - pdata.Value = ReadObject(stream); - } - - dict[key] = pdatas; - } - - return dict; - } - - public void WriteTo(IDictionary value, Stream stream) - { - // write properties count - PrimitiveSerializer.Int32.WriteTo(value.Count, stream); - - // write each property - foreach ((var alias, PropertyData[] values) in value) - { - // write alias - PrimitiveSerializer.String.WriteTo(alias, stream); - - // write values count - PrimitiveSerializer.Int32.WriteTo(values.Length, stream); - - // write each value - foreach (PropertyData pdata in values) - { - // everything that can be null is read/written as object - // even though - culture and segment should never be null here, - // see note in ReadFrom() method above - WriteObject(pdata.Culture ?? string.Empty, stream); - WriteObject(pdata.Segment ?? string.Empty, stream); - WriteObject(pdata.Value, stream); - } - } - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs deleted file mode 100644 index 14976717be..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs +++ /dev/null @@ -1,86 +0,0 @@ -using CSharpTest.Net.Collections; -using CSharpTest.Net.Serialization; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Exceptions; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -public class BTree -{ - public static BPlusTree GetTree(string filepath, bool exists, NuCacheSettings settings, ContentDataSerializer? contentDataSerializer = null) - { - var keySerializer = new PrimitiveSerializer(); - var valueSerializer = new ContentNodeKitSerializer(contentDataSerializer); - var options = new BPlusTree.OptionsV2(keySerializer, valueSerializer) - { - CreateFile = exists ? CreatePolicy.IfNeeded : CreatePolicy.Always, - FileName = filepath, - - // read or write but do *not* keep in memory - CachePolicy = CachePolicy.None, - - // default is 4096, min 2^9 = 512, max 2^16 = 64K - FileBlockSize = GetBlockSize(settings), - - // other options? - }; - - var tree = new BPlusTree(options); - - // anything? - // btree. - return tree; - } - - private static int GetBlockSize(NuCacheSettings settings) - { - var blockSize = 4096; - - var appSetting = settings.BTreeBlockSize; - if (!appSetting.HasValue) - { - return blockSize; - } - - blockSize = appSetting.Value; - - var bit = 0; - for (var i = blockSize; i != 1; i >>= 1) - { - bit++; - } - - if (1 << bit != blockSize) - { - throw new ConfigurationException($"Invalid block size value \"{blockSize}\": must be a power of two."); - } - - if (blockSize < 512 || blockSize > 65536) - { - throw new ConfigurationException($"Invalid block size value \"{blockSize}\": must be >= 512 and <= 65536."); - } - - return blockSize; - } - - /* - class ListOfIntSerializer : ISerializer> - { - public List ReadFrom(Stream stream) - { - var list = new List(); - var count = PrimitiveSerializer.Int32.ReadFrom(stream); - for (var i = 0; i < count; i++) - list.Add(PrimitiveSerializer.Int32.ReadFrom(stream)); - return list; - } - - public void WriteTo(List value, Stream stream) - { - PrimitiveSerializer.Int32.WriteTo(value.Count, stream); - foreach (var item in value) - PrimitiveSerializer.Int32.WriteTo(item, stream); - } - } - */ -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataModel.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataModel.cs deleted file mode 100644 index 5964a7aa8f..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataModel.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Runtime.Serialization; -using System.Text.Json.Serialization; -using MessagePack; -using Umbraco.Cms.Infrastructure.Serialization; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -/// -/// The content model stored in the content cache database table serialized as JSON -/// -[DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys -public class ContentCacheDataModel -{ - // TODO: We don't want to allocate empty arrays - // dont serialize empty properties - [DataMember(Order = 0)] - [JsonPropertyName("pd")] - [JsonConverter(typeof(JsonDictionaryStringInternIgnoreCaseConverter))] - [MessagePackFormatter(typeof(MessagePackDictionaryStringInternIgnoreCaseFormatter))] - public Dictionary? PropertyData { get; set; } - - [DataMember(Order = 1)] - [JsonPropertyName("cd")] - [JsonConverter(typeof(JsonDictionaryStringInternIgnoreCaseConverter))] - [MessagePackFormatter(typeof(MessagePackDictionaryStringInternIgnoreCaseFormatter))] - public Dictionary? CultureData { get; set; } - - [DataMember(Order = 2)] - [JsonPropertyName("us")] - public string? UrlSegment { get; set; } - - // Legacy properties used to deserialize existing nucache db entries - [IgnoreDataMember] - [JsonPropertyName("properties")] - [JsonConverter(typeof(JsonDictionaryStringIgnoreCaseConverter))] - private Dictionary LegacyPropertyData { set => PropertyData = value; } - - [IgnoreDataMember] - [JsonPropertyName("cultureData")] - [JsonConverter(typeof(JsonDictionaryStringIgnoreCaseConverter))] - private Dictionary LegacyCultureData { set => CultureData = value; } - - [IgnoreDataMember] - [JsonPropertyName("urlSegment")] - private string LegacyUrlSegment { set => UrlSegment = value; } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializationResult.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializationResult.cs deleted file mode 100644 index 822ecef194..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializationResult.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -/// -/// The serialization result from for which the serialized value -/// will be either a string or a byte[] -/// -public struct ContentCacheDataSerializationResult : IEquatable -{ - public ContentCacheDataSerializationResult(string? stringData, byte[]? byteData) - { - StringData = stringData; - ByteData = byteData; - } - - public string? StringData { get; } - - public byte[]? ByteData { get; } - - public static bool operator ==(ContentCacheDataSerializationResult left, ContentCacheDataSerializationResult right) - => left.Equals(right); - - public static bool operator !=(ContentCacheDataSerializationResult left, ContentCacheDataSerializationResult right) - => !(left == right); - - public override bool Equals(object? obj) - => obj is ContentCacheDataSerializationResult result && Equals(result); - - public bool Equals(ContentCacheDataSerializationResult other) - => StringData == other.StringData && - EqualityComparer.Default.Equals(ByteData, other.ByteData); - - public override int GetHashCode() - { - var hashCode = 1910544615; - if (StringData is not null) - { - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(StringData); - } - - if (ByteData is not null) - { - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ByteData); - } - - return hashCode; - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializerEntityType.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializerEntityType.cs deleted file mode 100644 index 1bb84a16d5..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializerEntityType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -[Flags] -public enum ContentCacheDataSerializerEntityType -{ - Document = 1, - Media = 2, - Member = 4, -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentData.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentData.cs deleted file mode 100644 index a7e0b60373..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentData.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -/// -/// Represents everything that is specific to an edited or published content version -/// -public class ContentData -{ - public ContentData(string? name, string? urlSegment, int versionId, DateTime versionDate, int writerId, int? templateId, bool published, IDictionary? properties, IReadOnlyDictionary? cultureInfos) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - UrlSegment = urlSegment; - VersionId = versionId; - VersionDate = versionDate; - WriterId = writerId; - TemplateId = templateId; - Published = published; - Properties = properties ?? throw new ArgumentNullException(nameof(properties)); - CultureInfos = cultureInfos; - } - - public string Name { get; } - public string? UrlSegment { get; } - public int VersionId { get; } - public DateTime VersionDate { get; } - public int WriterId { get; } - public int? TemplateId { get; } - public bool Published { get; } - - public IDictionary Properties { get; } - - /// - /// The collection of language Id to name for the content item - /// - public IReadOnlyDictionary? CultureInfos { get; } -} - diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs deleted file mode 100644 index b30f0e9661..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Umbraco.Cms.Core.Models; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource -{ - // read-only dto - internal class ContentSourceDto : IReadOnlyContentBase - { - public int Id { get; set; } - - public Guid Key { get; set; } - - public int ContentTypeId { get; set; } - - public int Level { get; set; } - - public string Path { get; set; } = string.Empty; - - public int SortOrder { get; set; } - - public int ParentId { get; set; } - - public bool Published { get; set; } - - public bool Edited { get; set; } - - public DateTime CreateDate { get; set; } - - public int CreatorId { get; set; } - - // edited data - public int VersionId { get; set; } - - public string? EditName { get; set; } - - public DateTime EditVersionDate { get; set; } - - public int EditWriterId { get; set; } - - public int EditTemplateId { get; set; } - - public string? EditData { get; set; } - - public byte[]? EditDataRaw { get; set; } - - // published data - public int PublishedVersionId { get; set; } - - public string? PubName { get; set; } - - public DateTime PubVersionDate { get; set; } - - public int PubWriterId { get; set; } - - public int PubTemplateId { get; set; } - - public string? PubData { get; set; } - - public byte[]? PubDataRaw { get; set; } - - // Explicit implementation - DateTime IReadOnlyContentBase.UpdateDate => EditVersionDate; - - string? IReadOnlyContentBase.Name => EditName; - - int IReadOnlyContentBase.WriterId => EditWriterId; - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs deleted file mode 100644 index ccc3799d3f..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Runtime.Serialization; -using System.Text.Json.Serialization; -using Umbraco.Cms.Infrastructure.Serialization; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -/// -/// Represents the culture variation information on a content item -/// -[DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys -public class CultureVariation -{ - [DataMember(Order = 0)] - [JsonPropertyName("nm")] - public string? Name { get; set; } - - [DataMember(Order = 1)] - [JsonPropertyName("us")] - public string? UrlSegment { get; set; } - - [DataMember(Order = 2)] - [JsonPropertyName("dt")] - [JsonConverter(typeof(JsonUniversalDateTimeConverter))] - public DateTime Date { get; set; } - - [DataMember(Order = 3)] - [JsonPropertyName("isd")] - public bool IsDraft { get; set; } - - // Legacy properties used to deserialize existing nucache db entries - [IgnoreDataMember] - [JsonPropertyName("nam")] - private string LegacyName { set => Name = value; } - - [IgnoreDataMember] - [JsonPropertyName("urlSegment")] - private string LegacyUrlSegment { set => UrlSegment = value; } - - [IgnoreDataMember] - [JsonPropertyName("date")] - [JsonConverter(typeof(JsonUniversalDateTimeConverter))] - private DateTime LegacyDate { set => Date = value; } - - [IgnoreDataMember] - [JsonPropertyName("isDraft")] - private bool LegacyIsDraft { set => IsDraft = value; } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs deleted file mode 100644 index b1c5b75b58..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Umbraco.Cms.Core.Models; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -/// -/// Serializes/Deserializes document to the SQL Database as a string -/// -/// -/// Resolved from the . This cannot be resolved from DI. -/// -public interface IContentCacheDataSerializer -{ - /// - /// Deserialize the data into a - /// - ContentCacheDataModel? Deserialize(IReadOnlyContentBase content, string? stringData, byte[]? byteData, - bool published); - - /// - /// Serializes the - /// - ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, - bool published); -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializerFactory.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializerFactory.cs deleted file mode 100644 index 09dd6ee73d..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializerFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -public interface IContentCacheDataSerializerFactory -{ - /// - /// Gets or creates a new instance of - /// - /// - /// - /// This method may return the same instance, however this depends on the state of the application and if any - /// underlying data has changed. - /// This method may also be used to initialize anything before a serialization/deserialization session occurs. - /// - IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types); -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs deleted file mode 100644 index 0215bd9fa8..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -public interface IDictionaryOfPropertyDataSerializer -{ - IDictionary ReadFrom(Stream stream); - - void WriteTo(IDictionary value, Stream stream); -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs deleted file mode 100644 index f0ab08c32a..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Umbraco.Cms.Core.Models; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -public class JsonContentNestedDataSerializer : IContentCacheDataSerializer -{ - private static readonly JsonSerializerOptions _jsonSerializerOptions = new() - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - - /// - public ContentCacheDataModel? Deserialize( - IReadOnlyContentBase content, - string? stringData, - byte[]? byteData, - bool published) - { - if (stringData == null && byteData != null) - { - throw new NotSupportedException( - $"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); - } - - return JsonSerializer.Deserialize(stringData!, _jsonSerializerOptions); - } - - /// - public ContentCacheDataSerializationResult Serialize( - IReadOnlyContentBase content, - ContentCacheDataModel model, - bool published) - { - var json = JsonSerializer.Serialize(model, _jsonSerializerOptions); - return new ContentCacheDataSerializationResult(json, null); - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs deleted file mode 100644 index 35e49bcf86..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -internal class JsonContentNestedDataSerializerFactory : IContentCacheDataSerializerFactory -{ - private readonly Lazy _serializer = new(); - - public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types) => _serializer.Value; -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/LazyCompressedString.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/LazyCompressedString.cs deleted file mode 100644 index e4dd8ccd3c..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/LazyCompressedString.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Diagnostics; -using System.Text; -using K4os.Compression.LZ4; -using Umbraco.Cms.Core.Exceptions; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -/// -/// Lazily decompresses a LZ4 Pickler compressed UTF8 string -/// -[DebuggerDisplay("{Display}")] -internal struct LazyCompressedString -{ - private readonly object _locker; - private byte[]? _bytes; - private string? _str; - - /// - /// Constructor - /// - /// LZ4 Pickle compressed UTF8 String - public LazyCompressedString(byte[] bytes) - { - _locker = new object(); - _bytes = bytes; - _str = null; - } - - /// - /// Used to display debugging output since ToString() can only be called once - /// - private string Display - { - get - { - if (_str != null) - { - return $"Decompressed: {_str}"; - } - - lock (_locker) - { - if (_str != null) - { - // double check - return $"Decompressed: {_str}"; - } - - if (_bytes == null) - { - // This shouldn't happen - throw new PanicException("Bytes have already been cleared"); - } - - return $"Compressed Bytes: {_bytes.Length}"; - } - } - } - - public static implicit operator string(LazyCompressedString l) => l.ToString(); - - public byte[] GetBytes() - { - if (_bytes == null) - { - throw new InvalidOperationException("The bytes have already been expanded"); - } - - return _bytes; - } - - /// - /// Returns the decompressed string from the bytes. This methods can only be called once. - /// - /// - /// Throws if this is called more than once - public string DecompressString() - { - if (_str != null) - { - return _str; - } - - lock (_locker) - { - if (_str != null) - { - // double check - return _str; - } - - if (_bytes == null) - { - throw new InvalidOperationException("Bytes have already been cleared"); - } - - _str = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(_bytes)); - _bytes = null; - } - - return _str; - } - - public override string ToString() => DecompressString(); -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/MessagePackDictionaryStringInternIgnoreCaseFormatter.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/MessagePackDictionaryStringInternIgnoreCaseFormatter.cs deleted file mode 100644 index 6b24962792..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/MessagePackDictionaryStringInternIgnoreCaseFormatter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using MessagePack; -using MessagePack.Formatters; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -/// -/// A MessagePack formatter (deserializer) for a string key dictionary that uses for the key string comparison and interns the string. -/// -/// The type of the value. -public sealed class MessagePackDictionaryStringInternIgnoreCaseFormatter : DictionaryFormatterBase, Dictionary.Enumerator, Dictionary> -{ - /// - protected override void Add(Dictionary collection, int index, string key, TValue value, MessagePackSerializerOptions options) - => collection.Add(string.Intern(key), value); - - /// - protected override Dictionary Complete(Dictionary intermediateCollection) - => intermediateCollection; - - /// - protected override Dictionary.Enumerator GetSourceEnumerator(Dictionary source) - => source.GetEnumerator(); - - /// - protected override Dictionary Create(int count, MessagePackSerializerOptions options) - => new(count, StringComparer.OrdinalIgnoreCase); -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs deleted file mode 100644 index c0a4718c40..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Text; -using K4os.Compression.LZ4; -using MessagePack; -using MessagePack.Resolvers; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.PropertyEditors; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -/// -/// Serializes/Deserializes document to the SQL Database as bytes using -/// MessagePack -/// -public class MsgPackContentNestedDataSerializer : IContentCacheDataSerializer -{ - private readonly MessagePackSerializerOptions _options; - private readonly IPropertyCacheCompression _propertyOptions; - - public MsgPackContentNestedDataSerializer(IPropertyCacheCompression propertyOptions) - { - _propertyOptions = propertyOptions ?? throw new ArgumentNullException(nameof(propertyOptions)); - - MessagePackSerializerOptions? defaultOptions = ContractlessStandardResolver.Options; - IFormatterResolver? resolver = CompositeResolver.Create( - - // TODO: We want to be able to intern the strings for aliases when deserializing like we do for Newtonsoft but I'm unsure exactly how - // to do that but it would seem to be with a custom message pack resolver but I haven't quite figured out based on the docs how - // to do that since that is part of the int key -> string mapping operation, might have to see the source code to figure that one out. - // There are docs here on how to build one of these: https://github.com/neuecc/MessagePack-CSharp/blob/master/README.md#low-level-api-imessagepackformattert - // and there are a couple examples if you search on google for them but this will need to be a separate project. - // NOTE: resolver custom types first - // new ContentNestedDataResolver(), - - // finally use standard resolver - defaultOptions.Resolver); - - _options = defaultOptions - .WithResolver(resolver) - .WithCompression(MessagePackCompression.Lz4BlockArray) - .WithSecurity(MessagePackSecurity.UntrustedData); - } - - public ContentCacheDataModel? Deserialize(IReadOnlyContentBase content, string? stringData, byte[]? byteData, bool published) - { - if (byteData != null) - { - ContentCacheDataModel? cacheModel = - MessagePackSerializer.Deserialize(byteData, _options); - Expand(content, cacheModel, published); - return cacheModel; - } - - if (stringData != null) - { - // NOTE: We don't really support strings but it's possible if manually used (i.e. tests) - var bin = Convert.FromBase64String(stringData); - ContentCacheDataModel? cacheModel = MessagePackSerializer.Deserialize(bin, _options); - Expand(content, cacheModel, published); - return cacheModel; - } - - return null; - } - - public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) - { - Compress(content, model, published); - var bytes = MessagePackSerializer.Serialize(model, _options); - return new ContentCacheDataSerializationResult(null, bytes); - } - - public string ToJson(byte[] bin) - { - var json = MessagePackSerializer.ConvertToJson(bin, _options); - return json; - } - - /// - /// Used during serialization to compress properties - /// - /// - /// - /// - /// - /// This will essentially 'double compress' property data. The MsgPack data as a whole will already be compressed - /// but this will go a step further and double compress property data so that it is stored in the nucache file - /// as compressed bytes and therefore will exist in memory as compressed bytes. That is, until the bytes are - /// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant - /// memory savings but could also affect performance of first rendering pages while decompression occurs. - /// - private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) - { - if (model.PropertyData is null) - { - return; - } - - foreach (KeyValuePair propertyAliasToData in model.PropertyData) - { - if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key, published)) - { - foreach (PropertyData property in propertyAliasToData.Value.Where(x => - x.Value != null && x.Value is string)) - { - if (property.Value is string propertyValue) - { - property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes(propertyValue)); - } - } - - foreach (PropertyData property in propertyAliasToData.Value.Where(x => - x.Value != null && x.Value is int intVal)) - { - property.Value = Convert.ToBoolean((int?)property.Value); - } - } - } - } - - /// - /// Used during deserialization to map the property data as lazy or expand the value - /// - /// - /// - /// - private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData, bool published) - { - if (nestedData.PropertyData is null) - { - return; - } - - foreach (KeyValuePair propertyAliasToData in nestedData.PropertyData) - { - if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key, published)) - { - foreach (PropertyData property in propertyAliasToData.Value.Where(x => x.Value != null)) - { - if (property.Value is byte[] byteArrayValue) - { - property.Value = new LazyCompressedString(byteArrayValue); - } - } - } - } - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs deleted file mode 100644 index e49a3935c1..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.Concurrent; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Services; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -internal class MsgPackContentNestedDataSerializerFactory : IContentCacheDataSerializerFactory -{ - private readonly IPropertyCacheCompressionOptions _compressionOptions; - private readonly IContentTypeService _contentTypeService; - private readonly ConcurrentDictionary<(int, string, bool), bool> _isCompressedCache = new(); - private readonly IMediaTypeService _mediaTypeService; - private readonly IMemberTypeService _memberTypeService; - private readonly PropertyEditorCollection _propertyEditors; - - public MsgPackContentNestedDataSerializerFactory( - IContentTypeService contentTypeService, - IMediaTypeService mediaTypeService, - IMemberTypeService memberTypeService, - PropertyEditorCollection propertyEditors, - IPropertyCacheCompressionOptions compressionOptions) - { - _contentTypeService = contentTypeService; - _mediaTypeService = mediaTypeService; - _memberTypeService = memberTypeService; - _propertyEditors = propertyEditors; - _compressionOptions = compressionOptions; - } - - public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types) - { - // Depending on which entity types are being requested, we need to look up those content types - // to initialize the compression options. - // We need to initialize these options now so that any data lookups required are completed and are not done while the content cache - // is performing DB queries which will result in errors since we'll be trying to query with open readers. - // NOTE: The calls to GetAll() below should be cached if the data has not been changed. - var contentTypes = new Dictionary(); - if ((types & ContentCacheDataSerializerEntityType.Document) == ContentCacheDataSerializerEntityType.Document) - { - foreach (IContentType ct in _contentTypeService.GetAll()) - { - contentTypes[ct.Id] = ct; - } - } - - if ((types & ContentCacheDataSerializerEntityType.Media) == ContentCacheDataSerializerEntityType.Media) - { - foreach (IMediaType ct in _mediaTypeService.GetAll()) - { - contentTypes[ct.Id] = ct; - } - } - - if ((types & ContentCacheDataSerializerEntityType.Member) == ContentCacheDataSerializerEntityType.Member) - { - foreach (IMemberType ct in _memberTypeService.GetAll()) - { - contentTypes[ct.Id] = ct; - } - } - - var compression = - new PropertyCacheCompression(_compressionOptions, contentTypes, _propertyEditors, _isCompressedCache); - var serializer = new MsgPackContentNestedDataSerializer(compression); - - return serializer; - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs deleted file mode 100644 index fde41dd90c..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.ComponentModel; -using System.Runtime.Serialization; -using System.Text.Json.Serialization; -using Umbraco.Cms.Infrastructure.Serialization; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -[DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys -public class PropertyData -{ - private string? _culture; - private string? _segment; - - [DataMember(Order = 0)] - [JsonConverter(typeof(JsonStringInternConverter))] - [DefaultValue("")] - [JsonPropertyName("c")] - public string? Culture - { - get => _culture; - set => _culture = - value ?? throw new ArgumentNullException( - nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null - } - - [DataMember(Order = 1)] - [JsonConverter(typeof(JsonStringInternConverter))] - [DefaultValue("")] - [JsonPropertyName("s")] - public string? Segment - { - get => _segment; - set => _segment = - value ?? throw new ArgumentNullException( - nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null - } - - [DataMember(Order = 2)] - [JsonPropertyName("v")] - public object? Value { get; set; } - - // Legacy properties used to deserialize existing nucache db entries - [IgnoreDataMember] - private string LegacyCulture - { - set => Culture = value; - } - - [IgnoreDataMember] - [JsonPropertyName("seg")] - private string LegacySegment - { - set => Segment = value; - } - - [IgnoreDataMember] - [JsonPropertyName("val")] - private object LegacyValue - { - set => Value = value; - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/SerializerBase.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/SerializerBase.cs deleted file mode 100644 index 5743975656..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/SerializerBase.cs +++ /dev/null @@ -1,249 +0,0 @@ -using CSharpTest.Net.Serialization; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -internal abstract class SerializerBase -{ - private const char PrefixNull = 'N'; - private const char PrefixString = 'S'; - private const char PrefixInt32 = 'I'; - private const char PrefixUInt16 = 'H'; - private const char PrefixUInt32 = 'J'; - private const char PrefixLong = 'L'; - private const char PrefixFloat = 'F'; - private const char PrefixDouble = 'B'; - private const char PrefixDateTime = 'D'; - private const char PrefixByte = 'O'; - private const char PrefixByteArray = 'A'; - private const char PrefixCompressedStringByteArray = 'C'; - private const char PrefixSignedByte = 'E'; - private const char PrefixBool = 'M'; - private const char PrefixGuid = 'G'; - private const char PrefixTimeSpan = 'T'; - private const char PrefixInt16 = 'Q'; - private const char PrefixChar = 'R'; - - protected string ReadString(Stream stream) => PrimitiveSerializer.String.ReadFrom(stream); - - protected int ReadInt(Stream stream) => PrimitiveSerializer.Int32.ReadFrom(stream); - - protected long ReadLong(Stream stream) => PrimitiveSerializer.Int64.ReadFrom(stream); - - protected float ReadFloat(Stream stream) => PrimitiveSerializer.Float.ReadFrom(stream); - - protected double ReadDouble(Stream stream) => PrimitiveSerializer.Double.ReadFrom(stream); - - protected DateTime ReadDateTime(Stream stream) => PrimitiveSerializer.DateTime.ReadFrom(stream); - - protected byte[] ReadByteArray(Stream stream) => PrimitiveSerializer.Bytes.ReadFrom(stream); - - protected string? ReadStringObject(Stream stream, bool intern = false) // required 'cos string is not a struct - { - var type = PrimitiveSerializer.Char.ReadFrom(stream); - if (type == PrefixNull) - { - return null; - } - - if (type != PrefixString) - { - throw new NotSupportedException($"Cannot deserialize type '{type}', expected '{PrefixString}'."); - } - - return intern - ? string.Intern(PrimitiveSerializer.String.ReadFrom(stream)) - : PrimitiveSerializer.String.ReadFrom(stream); - } - - private T? ReadStruct(Stream stream, char t, Func read) - where T : struct - { - var type = PrimitiveSerializer.Char.ReadFrom(stream); - if (type == PrefixNull) - { - return null; - } - - if (type != t) - { - throw new NotSupportedException($"Cannot deserialize type '{type}', expected '{t}'."); - } - - return read(stream); - } - - protected int? ReadIntObject(Stream stream) => ReadStruct(stream, PrefixInt32, ReadInt); - - protected long? ReadLongObject(Stream stream) => ReadStruct(stream, PrefixLong, ReadLong); - - protected float? ReadFloatObject(Stream stream) => ReadStruct(stream, PrefixFloat, ReadFloat); - - protected double? ReadDoubleObject(Stream stream) => ReadStruct(stream, PrefixDouble, ReadDouble); - - protected DateTime? ReadDateTimeObject(Stream stream) => ReadStruct(stream, PrefixDateTime, ReadDateTime); - - protected object? ReadObject(Stream stream) - => ReadObject(PrimitiveSerializer.Char.ReadFrom(stream), stream); - - /// - /// Reads in a value based on its char type - /// - /// - /// - /// - /// - /// This will incur boxing because the result is an object but in most cases the value will be a struct. - /// When the type is known use the specific methods like instead - /// - protected object? ReadObject(char type, Stream stream) - { - switch (type) - { - case PrefixNull: - return null; - case PrefixString: - return PrimitiveSerializer.String.ReadFrom(stream); - case PrefixInt32: - return PrimitiveSerializer.Int32.ReadFrom(stream); - case PrefixUInt16: - return PrimitiveSerializer.UInt16.ReadFrom(stream); - case PrefixUInt32: - return PrimitiveSerializer.UInt32.ReadFrom(stream); - case PrefixByte: - return PrimitiveSerializer.Byte.ReadFrom(stream); - case PrefixLong: - return PrimitiveSerializer.Int64.ReadFrom(stream); - case PrefixFloat: - return PrimitiveSerializer.Float.ReadFrom(stream); - case PrefixDouble: - return PrimitiveSerializer.Double.ReadFrom(stream); - case PrefixDateTime: - return PrimitiveSerializer.DateTime.ReadFrom(stream); - case PrefixByteArray: - return PrimitiveSerializer.Bytes.ReadFrom(stream); - case PrefixSignedByte: - return PrimitiveSerializer.SByte.ReadFrom(stream); - case PrefixBool: - return PrimitiveSerializer.Boolean.ReadFrom(stream); - case PrefixGuid: - return PrimitiveSerializer.Guid.ReadFrom(stream); - case PrefixTimeSpan: - return PrimitiveSerializer.TimeSpan.ReadFrom(stream); - case PrefixInt16: - return PrimitiveSerializer.Int16.ReadFrom(stream); - case PrefixChar: - return PrimitiveSerializer.Char.ReadFrom(stream); - case PrefixCompressedStringByteArray: - return new LazyCompressedString(PrimitiveSerializer.Bytes.ReadFrom(stream)); - default: - throw new NotSupportedException($"Cannot deserialize unknown type '{type}'."); - } - } - - /// - /// Writes a value to the stream ensuring it's char type is prefixed to the value for reading later - /// - /// - /// - /// - /// This method will incur boxing if the value is a struct. When the type is known use the - /// - /// to write the value directly. - /// - protected void WriteObject(object? value, Stream stream) - { - if (value == null) - { - PrimitiveSerializer.Char.WriteTo(PrefixNull, stream); - } - else if (value is string stringValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixString, stream); - PrimitiveSerializer.String.WriteTo(stringValue, stream); - } - else if (value is int intValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixInt32, stream); - PrimitiveSerializer.Int32.WriteTo(intValue, stream); - } - else if (value is byte byteValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixByte, stream); - PrimitiveSerializer.Byte.WriteTo(byteValue, stream); - } - else if (value is ushort ushortValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixUInt16, stream); - PrimitiveSerializer.UInt16.WriteTo(ushortValue, stream); - } - else if (value is long longValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixLong, stream); - PrimitiveSerializer.Int64.WriteTo(longValue, stream); - } - else if (value is float floatValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixFloat, stream); - PrimitiveSerializer.Float.WriteTo(floatValue, stream); - } - else if (value is double doubleValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixDouble, stream); - PrimitiveSerializer.Double.WriteTo(doubleValue, stream); - } - else if (value is DateTime dateValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixDateTime, stream); - PrimitiveSerializer.DateTime.WriteTo(dateValue, stream); - } - else if (value is uint uInt32Value) - { - PrimitiveSerializer.Char.WriteTo(PrefixUInt32, stream); - PrimitiveSerializer.UInt32.WriteTo(uInt32Value, stream); - } - else if (value is byte[] byteArrayValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixByteArray, stream); - PrimitiveSerializer.Bytes.WriteTo(byteArrayValue, stream); - } - else if (value is LazyCompressedString lazyCompressedString) - { - PrimitiveSerializer.Char.WriteTo(PrefixCompressedStringByteArray, stream); - PrimitiveSerializer.Bytes.WriteTo(lazyCompressedString.GetBytes(), stream); - } - else if (value is sbyte signedByteValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixSignedByte, stream); - PrimitiveSerializer.SByte.WriteTo(signedByteValue, stream); - } - else if (value is bool boolValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixBool, stream); - PrimitiveSerializer.Boolean.WriteTo(boolValue, stream); - } - else if (value is Guid guidValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixGuid, stream); - PrimitiveSerializer.Guid.WriteTo(guidValue, stream); - } - else if (value is TimeSpan timespanValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixTimeSpan, stream); - PrimitiveSerializer.TimeSpan.WriteTo(timespanValue, stream); - } - else if (value is short int16Value) - { - PrimitiveSerializer.Char.WriteTo(PrefixInt16, stream); - PrimitiveSerializer.Int16.WriteTo(int16Value, stream); - } - else if (value is char charValue) - { - PrimitiveSerializer.Char.WriteTo(PrefixChar, stream); - PrimitiveSerializer.Char.WriteTo(charValue, stream); - } - else - { - throw new NotSupportedException("Value type " + value.GetType().FullName + " cannot be serialized."); - } - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs deleted file mode 100644 index 0c1042c3ff..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; - -namespace Umbraco.Extensions; - -/// -/// Extension methods for for the Umbraco's NuCache -/// -public static class UmbracoBuilderExtensions -{ - /// - /// Adds Umbraco NuCache dependencies - /// - public static IUmbracoBuilder AddNuCache(this IUmbracoBuilder builder) - { - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - - // register the NuCache published snapshot service - // must register default options, required in the service ctor - builder.Services.TryAddTransient(factory => new PublishedSnapshotServiceOptions()); - builder.SetPublishedSnapshotService(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddTransient(); - - // replace this service since we want to improve the content/media - // mapping lookups if we are using nucache. - // TODO: Gotta wonder how much this does actually improve perf? It's a lot of weird code to make this happen so hope it's worth it - builder.Services.AddUnique(factory => - { - var idkSvc = new IdKeyMap( - factory.GetRequiredService(), - factory.GetRequiredService()); - if (factory.GetRequiredService() is PublishedSnapshotService - publishedSnapshotService) - { - idkSvc.SetMapper(UmbracoObjectTypes.Document, id => publishedSnapshotService.GetDocumentUid(id), uid => publishedSnapshotService.GetDocumentId(uid)); - idkSvc.SetMapper(UmbracoObjectTypes.Media, id => publishedSnapshotService.GetMediaUid(id), uid => publishedSnapshotService.GetMediaId(uid)); - } - - return idkSvc; - }); - - builder.AddNuCacheNotifications(); - - builder.AddNotificationHandler(); - builder.Services.AddSingleton(s => - { - IOptions options = s.GetRequiredService>(); - switch (options.Value.NuCacheSerializerType) - { - case NuCacheSerializerType.JSON: - return new JsonContentNestedDataSerializerFactory(); - case NuCacheSerializerType.MessagePack: - return ActivatorUtilities.CreateInstance(s); - default: - throw new IndexOutOfRangeException(); - } - }); - - builder.Services.AddSingleton(s => - { - IOptions options = s.GetRequiredService>(); - - if (options.Value.NuCacheSerializerType == NuCacheSerializerType.MessagePack && - options.Value.UnPublishedContentCompression) - { - return new UnPublishedContentPropertyCacheCompressionOptions(); - } - - return new NoopPropertyCacheCompressionOptions(); - }); - - builder.Services.AddSingleton(s => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); - - // add the NuCache health check (hidden from type finder) - // TODO: no NuCache health check yet - // composition.HealthChecks().Add(); - return builder; - } - - private static IUmbracoBuilder AddNuCacheNotifications(this IUmbracoBuilder builder) - { - builder - .AddNotificationHandler() - .AddNotificationHandler() -#pragma warning disable CS0618 // Type or member is obsolete - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() -#pragma warning restore CS0618 // Type or member is obsolete - ; - - return builder; - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DomainCache.cs b/src/Umbraco.PublishedCache.NuCache/DomainCache.cs deleted file mode 100644 index 27d9cd35c8..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DomainCache.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -/// -/// Implements for NuCache. -/// -public class DomainCache : IDomainCache -{ - private readonly SnapDictionary.Snapshot _snapshot; - - /// - /// Initializes a new instance of the class. - /// - public DomainCache(SnapDictionary.Snapshot snapshot, string defaultCulture) - { - _snapshot = snapshot; - DefaultCulture = defaultCulture; - } - - /// - public string DefaultCulture { get; } - - /// - public IEnumerable GetAll(bool includeWildcards) - { - IEnumerable list = _snapshot.GetAll(); - if (includeWildcards == false) - { - list = list.Where(x => x.IsWildcard == false); - } - - return list.OrderBy(x => x.SortOrder); - } - - /// - public IEnumerable GetAssigned(int documentId, bool includeWildcards = false) - { - // probably this could be optimized with an index - // but then we'd need a custom DomainStore of some sort - IEnumerable list = _snapshot.GetAll(); - list = list.Where(x => x.ContentId == documentId); - if (includeWildcards == false) - { - list = list.Where(x => x.IsWildcard == false); - } - - return list.OrderBy(x => x.SortOrder); - } - - /// - public bool HasAssigned(int documentId, bool includeWildcards = false) - => documentId > 0 && GetAssigned(documentId, includeWildcards).Any(); -} diff --git a/src/Umbraco.PublishedCache.NuCache/DomainCacheExtensions.cs b/src/Umbraco.PublishedCache.NuCache/DomainCacheExtensions.cs deleted file mode 100644 index fb254aa5f6..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DomainCacheExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -public static class DomainCacheExtensions -{ - public static bool GetAssignedWithCulture(this IDomainCache domainCache, string? culture, int documentId, - bool includeWildcards = false) - { - IEnumerable assigned = domainCache.GetAssigned(documentId, includeWildcards); - - // It's super important that we always compare cultures with ignore case, since we can't be sure of the casing! - // Comparing with string.IsNullOrEmpty since both empty string and null signifies invariant. - return string.IsNullOrEmpty(culture) - ? assigned.Any() - : assigned.Any(x => x.Culture?.Equals(culture, StringComparison.InvariantCultureIgnoreCase) ?? false); - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/MediaCache.cs b/src/Umbraco.PublishedCache.NuCache/MediaCache.cs deleted file mode 100644 index 4c65255aa7..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/MediaCache.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.Navigable; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -public class MediaCache : PublishedCacheBase, IPublishedMediaCache, INavigableData, IDisposable -{ - private readonly ContentStore.Snapshot _snapshot; - private readonly IVariationContextAccessor _variationContextAccessor; - - #region Constructors - - public MediaCache(bool previewDefault, ContentStore.Snapshot snapshot, IVariationContextAccessor variationContextAccessor) - : base(variationContextAccessor, previewDefault) - { - _snapshot = snapshot; - _variationContextAccessor = variationContextAccessor; - } - - #endregion - - #region IDisposable - - public void Dispose() => _snapshot.Dispose(); - - #endregion - - #region Get, Has - - public override IPublishedContent? GetById(bool preview, int contentId) - { - // ignore preview, there's only draft for media - ContentNode? n = _snapshot.Get(contentId); - return n?.PublishedModel; - } - - public override IPublishedContent? GetById(bool preview, Guid contentId) - { - // ignore preview, there's only draft for media - ContentNode? n = _snapshot.Get(contentId); - return n?.PublishedModel; - } - - public override IPublishedContent? GetById(bool preview, Udi contentId) - { - var guidUdi = contentId as GuidUdi; - if (guidUdi == null) - { - throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId)); - } - - if (guidUdi.EntityType != Constants.UdiEntityType.Media) - { - throw new ArgumentException( - $"Udi entity type must be \"{Constants.UdiEntityType.Media}\".", - nameof(contentId)); - } - - // ignore preview, there's only draft for media - ContentNode? n = _snapshot.Get(guidUdi.Guid); - return n?.PublishedModel; - } - - public override bool HasById(bool preview, int contentId) - { - ContentNode? n = _snapshot.Get(contentId); - return n != null; - } - - IEnumerable INavigableData.GetAtRoot(bool preview) => GetAtRoot(preview); - - public override IEnumerable GetAtRoot(bool preview, string? culture = null) - { - // handle context culture for variant - if (culture == null) - { - culture = _variationContextAccessor?.VariationContext?.Culture ?? string.Empty; - } - - IEnumerable atRoot = _snapshot.GetAtRoot().Select(x => x.PublishedModel); - return culture == "*" - ? atRoot.WhereNotNull() - : atRoot.Where(x => x?.IsInvariantOrHasCulture(culture) ?? false).WhereNotNull(); - } - - public override bool HasContent(bool preview) => _snapshot.IsEmpty == false; - - #endregion - - #region Content types - - public override IPublishedContentType? GetContentType(int id) => _snapshot.GetContentType(id); - - public override IPublishedContentType? GetContentType(string alias) => _snapshot.GetContentType(alias); - - public override IPublishedContentType? GetContentType(Guid key) => _snapshot.GetContentType(key); - - #endregion - - public Task GetByIdAsync(int id) => throw new NotImplementedException(); - - public Task GetByKeyAsync(Guid key) => throw new NotImplementedException(); - - public Task HasByIdAsync(int id) => throw new NotImplementedException(); -} diff --git a/src/Umbraco.PublishedCache.NuCache/MemberCache.cs b/src/Umbraco.PublishedCache.NuCache/MemberCache.cs deleted file mode 100644 index 0aa89b2c42..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/MemberCache.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PublishedCache; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -public class MemberCache : IPublishedMemberCache, IDisposable -{ - private readonly PublishedContentTypeCache _contentTypeCache; - private readonly bool _previewDefault; - private readonly IPublishedModelFactory _publishedModelFactory; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly IVariationContextAccessor _variationContextAccessor; - private bool _disposedValue; - - public MemberCache( - bool previewDefault, - PublishedContentTypeCache contentTypeCache, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IPublishedModelFactory publishedModelFactory) - { - _publishedSnapshotAccessor = publishedSnapshotAccessor; - _variationContextAccessor = variationContextAccessor; - _publishedModelFactory = publishedModelFactory; - _previewDefault = previewDefault; - _contentTypeCache = contentTypeCache; - } - - #region Content types - - public IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Member, id); - - public IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Member, alias); - public Task GetAsync(IMember member) => throw new NotImplementedException(); - - public IPublishedMember? Get(IMember member) - => - (IPublishedMember?)PublishedMember.Create( - member, - GetContentType(member.ContentTypeId), - _previewDefault, - _publishedSnapshotAccessor, - _variationContextAccessor, - _publishedModelFactory); - - public void Dispose() => - - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(true); - - #endregion - - #region IDisposable - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - // _contentTypeCache.Dispose(); - } - - _disposedValue = true; - } - } - - #endregion -} diff --git a/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableContent.cs b/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableContent.cs deleted file mode 100644 index b2a8c83d3f..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableContent.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.Navigable; - -/// -/// Represents a content that can be navigated via XPath. -/// -[Obsolete("The current implementation of XPath is suboptimal and will be removed entirely in a future version. Scheduled for removal in v15. Still needed for NuCache")] -internal interface INavigableContent -{ - /// - /// Gets the unique identifier of the navigable content. - /// - /// The root node identifier should be -1. - int Id { get; } - - /// - /// Gets the unique identifier of parent of the navigable content. - /// - /// - /// The top-level content parent identifiers should be -1 ie the identifier - /// of the root node, whose parent identifier should in turn be -1. - /// - int ParentId { get; } - - /// - /// Gets the type of the navigable content. - /// - INavigableContentType Type { get; } - - /// - /// Gets the unique identifiers of the children of the navigable content. - /// - IList? ChildIds { get; } - - /// - /// Gets the value of a field of the navigable content for XPath navigation use. - /// - /// The field index. - /// The value of the field for XPath navigation use. - /// - /// - /// Fields are attributes or elements depending on their relative index value compared - /// to source.LastAttributeIndex. - /// - /// For attributes, the value must be a string. - /// - /// For elements, the value should an XPathNavigator instance if the field is xml - /// and has content (is not empty), null to indicate that the element is empty, or a string - /// which can be empty, whitespace... depending on what the data type wants to expose. - /// - /// - object? Value(int index); - - // TODO: implement the following one - - ///// - ///// Gets the value of a field of the navigable content, for a specified language. - ///// - ///// The field index. - ///// The language key. - ///// The value of the field for the specified language. - ///// ... - // object Value(int index, string languageKey); -} diff --git a/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableContentType.cs b/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableContentType.cs deleted file mode 100644 index e9af35056f..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableContentType.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.Navigable; - -/// -/// Represents the type of a content that can be navigated via XPath. -/// -[Obsolete("The current implementation of XPath is suboptimal and will be removed entirely in a future version. Scheduled for removal in v15. Still needed for NuCache")] -internal interface INavigableContentType -{ - /// - /// Gets the name of the content type. - /// - string? Name { get; } - - /// - /// Gets the field types of the content type. - /// - /// This includes the attributes and the properties. - INavigableFieldType[] FieldTypes { get; } -} diff --git a/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableData.cs b/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableData.cs deleted file mode 100644 index b860f26c93..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableData.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Umbraco.Cms.Core.Models.PublishedContent; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.Navigable; - -internal interface INavigableData -{ - IPublishedContent? GetById(bool preview, int contentId); - - IEnumerable GetAtRoot(bool preview); -} diff --git a/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableFieldType.cs b/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableFieldType.cs deleted file mode 100644 index 5bb87bb8a8..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableFieldType.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.Navigable; - -/// -/// Represents the type of a field of a content that can be navigated via XPath. -/// -/// A field can be an attribute or a property. -[Obsolete("The current implementation of XPath is suboptimal and will be removed entirely in a future version. Scheduled for removal in v15. Still needed for NuCache")] -internal interface INavigableFieldType -{ - /// - /// Gets the name of the field type. - /// - string Name { get; } - - /// - /// Gets a method to convert the field value to a string. - /// - /// - /// This is for built-in properties, ie attributes. User-defined properties have their - /// own way to convert their value for XPath. - /// - Func? XmlStringConverter { get; } -} diff --git a/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableSource.cs b/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableSource.cs deleted file mode 100644 index b391fde973..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Navigable/INavigableSource.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.Navigable; - -/// -/// Represents a source of content that can be navigated via XPath. -/// -[Obsolete("The current implementation of XPath is suboptimal and will be removed entirely in a future version. Scheduled for removal in v15. Still needed for NuCache")] -internal interface INavigableSource -{ - /// - /// Gets the index of the last attribute in the fields collections. - /// - int LastAttributeIndex { get; } - - /// - /// Gets the content at the root of the source. - /// - /// - /// That content should have unique identifier -1 and should not be gettable, - /// ie Get(-1) should return null. Its ParentId should be -1. It should provide - /// values for the attribute fields. - /// - INavigableContent Root { get; } - - /// - /// Gets a content identified by its unique identifier. - /// - /// The unique identifier. - /// The content identified by the unique identifier, or null. - /// When id is -1 (root content) implementations should return null. - INavigableContent? Get(int id); -} diff --git a/src/Umbraco.PublishedCache.NuCache/Navigable/NavigableContent.cs b/src/Umbraco.PublishedCache.NuCache/Navigable/NavigableContent.cs deleted file mode 100644 index e7fa034c9e..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Navigable/NavigableContent.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Umbraco.Cms.Core.Models.PublishedContent; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.Navigable; - -internal class NavigableContent : INavigableContent -{ - private readonly string?[] _builtInValues; - private readonly PublishedContent _content; - - public NavigableContent(IPublishedContent content) - { - InnerContent = content; - _content = PublishedContent.UnwrapIPublishedContent(InnerContent); - - var i = 0; - _builtInValues = new[] - { - XmlString(i++, _content.Name), XmlString(i++, _content.ParentId), XmlString(i++, _content.CreateDate), - XmlString(i++, _content.UpdateDate), XmlString(i++, true), // isDoc - XmlString(i++, _content.SortOrder), XmlString(i++, _content.Level), XmlString(i++, _content.TemplateId), - XmlString(i++, _content.WriterId), XmlString(i++, _content.CreatorId), XmlString(i++, _content.UrlSegment), - XmlString(i, _content.IsDraft()), - }; - } - - #region INavigableContent - - public IPublishedContent InnerContent { get; } - - private string? XmlString(int index, object? value) - { - if (value == null) - { - return string.Empty; - } - - INavigableFieldType field = Type.FieldTypes[index]; - return field.XmlStringConverter == null ? value.ToString() : field.XmlStringConverter(value); - } - - public int Id => _content.Id; - - public int ParentId => _content.ParentId; - - public INavigableContentType Type => NavigableContentType.GetContentType(_content.ContentType); - - // returns all child ids, will be filtered by the source - public IList? ChildIds => _content.ChildIds; - - public object? Value(int index) - { - if (index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - if (index < NavigableContentType.BuiltinProperties.Length) - { - // built-in field, ie attribute - // return XmlString(index, _builtInValues1[index]); - return _builtInValues[index]; - } - - // custom property, ie element - return null; - } - - #endregion -} diff --git a/src/Umbraco.PublishedCache.NuCache/Navigable/NavigableContentType.cs b/src/Umbraco.PublishedCache.NuCache/Navigable/NavigableContentType.cs deleted file mode 100644 index 3859bba571..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Navigable/NavigableContentType.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Xml; -using Umbraco.Cms.Core.Models.PublishedContent; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.Navigable; - -internal class NavigableContentType : INavigableContentType -{ - public static readonly INavigableFieldType[] BuiltinProperties; - - // note - PublishedContentType are immutable ie they do not _change_ when the actual IContentTypeComposition - // changes, but they are replaced by a new instance, so our map here will clean itself automatically and - // we don't have to manage cache - ConditionalWeakTable does not prevent keys from being GCed - private static readonly ConditionalWeakTable TypesMap = new(); - - private readonly object _locko = new(); - - static NavigableContentType() => - BuiltinProperties = new INavigableFieldType[] - { - new NavigablePropertyType("nodeName"), new NavigablePropertyType("parentId"), - new NavigablePropertyType("createDate", v => XmlConvert.ToString((DateTime)v, "yyyy-MM-ddTHH:mm:ss")), - new NavigablePropertyType("updateDate", v => XmlConvert.ToString((DateTime)v, "yyyy-MM-ddTHH:mm:ss")), - new NavigablePropertyType("isDoc", v => XmlConvert.ToString((bool)v)), - new NavigablePropertyType("sortOrder"), new NavigablePropertyType("level"), - new NavigablePropertyType("templateId"), new NavigablePropertyType("writerId"), - new NavigablePropertyType("creatorId"), new NavigablePropertyType("urlName"), - new NavigablePropertyType("isDraft", v => XmlConvert.ToString((bool)v)), - }; - - // called by the conditional weak table -- must be public - // ReSharper disable EmptyConstructor -#pragma warning disable CS8618 - public NavigableContentType() -#pragma warning restore CS8618 - - // ReSharper restore EmptyConstructor - { - } - - public string Name { get; private set; } - - public INavigableFieldType[] FieldTypes { get; private set; } - - public static NavigableContentType GetContentType(IPublishedContentType contentType) => - TypesMap.GetOrCreateValue(contentType).EnsureInitialized(contentType); - - private NavigableContentType EnsureInitialized(IPublishedContentType contentType) - { - lock (_locko) - { - if (Name == null) - { - Name = contentType.Alias; - FieldTypes = BuiltinProperties - .Union(contentType.PropertyTypes.Select(propertyType => - new NavigablePropertyType(propertyType.Alias))) - .ToArray(); - } - } - - return this; - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/Navigable/NavigablePropertyType.cs b/src/Umbraco.PublishedCache.NuCache/Navigable/NavigablePropertyType.cs deleted file mode 100644 index 48a630a5b2..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Navigable/NavigablePropertyType.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.Navigable; - -internal class NavigablePropertyType : INavigableFieldType -{ - public NavigablePropertyType(string name, Func? xmlStringConverter = null) - { - Name = name; - XmlStringConverter = xmlStringConverter; - } - - public string Name { get; } - - public Func? XmlStringConverter { get; } -} diff --git a/src/Umbraco.PublishedCache.NuCache/Navigable/RootContent.cs b/src/Umbraco.PublishedCache.NuCache/Navigable/RootContent.cs deleted file mode 100644 index 6085c582c3..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Navigable/RootContent.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.Navigable; - -internal class RootContent : INavigableContent -{ - private static readonly RootContentType ContentType = new(); - private readonly int[] _childIds; - - public RootContent(IEnumerable childIds) => _childIds = childIds.ToArray(); - - public int Id => -1; - - public int ParentId => -1; - - public INavigableContentType Type => ContentType; - - public IList ChildIds => _childIds; - - public object? Value(int index) => - - // only id has a value - index == 0 ? "-1" : null; - - private class RootContentType : INavigableContentType - { - public string Name => "root"; - - public INavigableFieldType[] FieldTypes => NavigableContentType.BuiltinProperties; - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/Navigable/Source.cs b/src/Umbraco.PublishedCache.NuCache/Navigable/Source.cs deleted file mode 100644 index bfe29b6c49..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Navigable/Source.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Umbraco.Cms.Core.Models.PublishedContent; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.Navigable; - -internal class Source : INavigableSource -{ - private readonly INavigableData _data; - private readonly bool _preview; - private readonly RootContent _root; - - public Source(INavigableData data, bool preview) - { - _data = data; - _preview = preview; - - IEnumerable contentAtRoot = data.GetAtRoot(preview); - _root = new RootContent(contentAtRoot.Select(x => x.Id)); - } - - public int LastAttributeIndex => NavigableContentType.BuiltinProperties.Length - 1; - - public INavigableContent? Get(int id) - { - // wrap in a navigable content - IPublishedContent? content = _data.GetById(_preview, id); - return content == null ? null : new NavigableContent(content); - } - - public INavigableContent Root => _root; -} diff --git a/src/Umbraco.PublishedCache.NuCache/NuCacheStartupHandler.cs b/src/Umbraco.PublishedCache.NuCache/NuCacheStartupHandler.cs deleted file mode 100644 index 86b4dce648..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/NuCacheStartupHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -/// -/// Rebuilds the database cache if required when the serializer changes -/// -public class NuCacheStartupHandler : INotificationHandler -{ - private readonly INuCacheContentService _nuCacheContentService; - private readonly IRuntimeState _runtimeState; - - public NuCacheStartupHandler( - INuCacheContentService nuCacheContentService, - IRuntimeState runtimeState) - { - _nuCacheContentService = nuCacheContentService; - _runtimeState = runtimeState; - } - - public void Handle(UmbracoApplicationStartingNotification notification) - { - if (_runtimeState.Level == RuntimeLevel.Run) - { - _nuCacheContentService.RebuildDatabaseCacheIfSerializerChanged(); - } - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentRepository.cs deleted file mode 100644 index 4d0cefe39b..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentRepository.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Umbraco.Cms.Core.Models; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence; - -public interface INuCacheContentRepository -{ - void DeleteContentItem(IContentBase item); - - IEnumerable GetAllContentSources(); - - IEnumerable GetAllMediaSources(); - - IEnumerable GetBranchContentSources(int id); - - IEnumerable GetBranchMediaSources(int id); - - ContentNodeKit GetContentSource(int id); - - ContentNodeKit GetMediaSource(int id); - - IEnumerable GetTypeContentSources(IEnumerable? ids); - - IEnumerable GetTypeMediaSources(IEnumerable ids); - - /// - /// Refreshes the nucache database row for the - /// - void RefreshContent(IContent content); - - /// - /// Refreshes the nucache database row for the - /// - void RefreshMedia(IMedia content); - - /// - /// Refreshes the nucache database row for the - /// - void RefreshMember(IMember content); - - /// - /// Rebuilds the caches for content, media and/or members based on the content type ids specified - /// - /// - /// If not null will process content for the matching content types, if empty will process all - /// content - /// - /// - /// If not null will process content for the matching media types, if empty will process all - /// media - /// - /// - /// If not null will process content for the matching members types, if empty will process all - /// members - /// - void Rebuild( - IReadOnlyCollection? contentTypeIds = null, - IReadOnlyCollection? mediaTypeIds = null, - IReadOnlyCollection? memberTypeIds = null); - - bool VerifyContentDbCache(); - - bool VerifyMediaDbCache(); - - bool VerifyMemberDbCache(); -} diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs deleted file mode 100644 index 231f8614b6..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Umbraco.Cms.Core.Models; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence; - -/// -/// Defines a data source for NuCache. -/// -public interface INuCacheContentService -{ - /// - /// Used during startup to see if the configured serialized is different from the persisted serialize type. - /// If they are different, this will rebuild the nucache DB table with the configured serializer. - /// - void RebuildDatabaseCacheIfSerializerChanged(); - - // TODO: For these required sort orders, would sorting on Path 'just work'? - ContentNodeKit GetContentSource(int id); - - /// - /// Returns all content ordered by level + sortOrder - /// - /// - /// MUST be ordered by level + parentId + sortOrder! - /// - IEnumerable GetAllContentSources(); - - /// - /// Returns branch for content ordered by level + sortOrder - /// - /// - /// MUST be ordered by level + parentId + sortOrder! - /// - IEnumerable GetBranchContentSources(int id); - - /// - /// Returns content by Ids ordered by level + sortOrder - /// - /// - /// MUST be ordered by level + parentId + sortOrder! - /// - IEnumerable GetTypeContentSources(IEnumerable? ids); - - ContentNodeKit GetMediaSource(int id); - - /// - /// Returns all media ordered by level + sortOrder - /// - /// - /// MUST be ordered by level + parentId + sortOrder! - /// - IEnumerable GetAllMediaSources(); - - /// - /// Returns branch for media ordered by level + sortOrder - /// - /// - /// MUST be ordered by level + parentId + sortOrder! - /// - IEnumerable GetBranchMediaSources(int id); // must order by level, sortOrder - - /// - /// Returns media by Ids ordered by level + sortOrder - /// - /// - /// MUST be ordered by level + parentId + sortOrder! - /// - IEnumerable GetTypeMediaSources(IEnumerable ids); - - void DeleteContentItem(IContentBase item); - - void DeleteContentItems(IEnumerable items); - - /// - /// Refreshes the nucache database row for the - /// - void RefreshContent(IContent content); - - /// - /// Refreshes the nucache database row for the - /// - void RefreshMedia(IMedia media); - - /// - /// Refreshes the nucache database row for the - /// - void RefreshMember(IMember member); - - /// - /// Rebuilds the database caches for content, media and/or members based on the content type ids specified - /// - /// - /// If not null will process content for the matching content types, if empty will process all - /// content - /// - /// - /// If not null will process content for the matching media types, if empty will process all - /// media - /// - /// - /// If not null will process content for the matching members types, if empty will process all - /// members - /// - void Rebuild( - IReadOnlyCollection? contentTypeIds = null, - IReadOnlyCollection? mediaTypeIds = null, - IReadOnlyCollection? memberTypeIds = null); - - bool VerifyContentDbCache(); - - bool VerifyMediaDbCache(); - - bool VerifyMemberDbCache(); -} diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs deleted file mode 100644 index a65b66cd30..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs +++ /dev/null @@ -1,1053 +0,0 @@ -using System.Diagnostics; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using NPoco; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Persistence.Querying; -using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; -using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Cms.Infrastructure.Scoping; -using Umbraco.Extensions; -using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence; - -public class NuCacheContentRepository : RepositoryBase, INuCacheContentRepository -{ - private readonly IContentCacheDataSerializerFactory _contentCacheDataSerializerFactory; - private readonly IDocumentRepository _documentRepository; - private readonly ILogger _logger; - private readonly IMediaRepository _mediaRepository; - private readonly IMemberRepository _memberRepository; - private readonly IOptions _nucacheSettings; - private readonly IShortStringHelper _shortStringHelper; - private readonly UrlSegmentProviderCollection _urlSegmentProviders; - - /// - /// Initializes a new instance of the class. - /// - public NuCacheContentRepository( - IScopeAccessor scopeAccessor, - AppCaches appCaches, - ILogger logger, - IMemberRepository memberRepository, - IDocumentRepository documentRepository, - IMediaRepository mediaRepository, - IShortStringHelper shortStringHelper, - UrlSegmentProviderCollection urlSegmentProviders, - IContentCacheDataSerializerFactory contentCacheDataSerializerFactory, - IOptions nucacheSettings) - : base(scopeAccessor, appCaches) - { - _logger = logger; - _memberRepository = memberRepository; - _documentRepository = documentRepository; - _mediaRepository = mediaRepository; - _shortStringHelper = shortStringHelper; - _urlSegmentProviders = urlSegmentProviders; - _contentCacheDataSerializerFactory = contentCacheDataSerializerFactory; - _nucacheSettings = nucacheSettings; - } - - public void DeleteContentItem(IContentBase item) - => Database.Execute("DELETE FROM cmsContentNu WHERE nodeId=@id", new { id = item.Id }); - - public void RefreshContent(IContent content) - { - IContentCacheDataSerializer serializer = - _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); - - // always refresh the edited data - OnRepositoryRefreshed(serializer, content, false); - - if (content.PublishedState == PublishedState.Unpublishing) - { - // if unpublishing, remove published data from table - Database.Execute("DELETE FROM cmsContentNu WHERE nodeId=@id AND published=1", new { id = content.Id }); - } - else if (content.PublishedState == PublishedState.Publishing) - { - // if publishing, refresh the published data - OnRepositoryRefreshed(serializer, content, true); - } - } - - public void RefreshMedia(IMedia media) - { - IContentCacheDataSerializer serializer = - _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); - - OnRepositoryRefreshed(serializer, media, false); - } - - public void RefreshMember(IMember member) - { - IContentCacheDataSerializer serializer = - _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Member); - - OnRepositoryRefreshed(serializer, member, false); - } - - /// - public void Rebuild( - IReadOnlyCollection? contentTypeIds = null, - IReadOnlyCollection? mediaTypeIds = null, - IReadOnlyCollection? memberTypeIds = null) - { - IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create( - ContentCacheDataSerializerEntityType.Document - | ContentCacheDataSerializerEntityType.Media - | ContentCacheDataSerializerEntityType.Member); - - // If contentTypeIds, mediaTypeIds and memberTypeIds are null, truncate table as all records will be deleted (as these 3 are the only types in the table). - if (contentTypeIds != null && !contentTypeIds.Any() - && mediaTypeIds != null && !mediaTypeIds.Any() - && memberTypeIds != null && !memberTypeIds.Any()) - { - if (Database.DatabaseType == DatabaseType.SqlServer2012) - { - Database.Execute($"TRUNCATE TABLE cmsContentNu"); - } - - if (Database.DatabaseType == DatabaseType.SQLite) - { - Database.Execute($"DELETE FROM cmsContentNu"); - } - } - - if (contentTypeIds != null) - { - RebuildContentDbCache(serializer, _nucacheSettings.Value.SqlPageSize, contentTypeIds); - } - - if (mediaTypeIds != null) - { - RebuildMediaDbCache(serializer, _nucacheSettings.Value.SqlPageSize, mediaTypeIds); - } - - if (memberTypeIds != null) - { - RebuildMemberDbCache(serializer, _nucacheSettings.Value.SqlPageSize, memberTypeIds); - } - } - - // assumes content tree lock - public bool VerifyContentDbCache() - { - // every document should have a corresponding row for edited properties - // and if published, may have a corresponding row for published properties - Guid contentObjectType = Constants.ObjectTypes.Document; - - var count = Database.ExecuteScalar( - $@"SELECT COUNT(*) -FROM umbracoNode -JOIN {Constants.DatabaseSchema.Tables.Document} ON umbracoNode.id={Constants.DatabaseSchema.Tables.Document}.nodeId -LEFT JOIN cmsContentNu nuEdited ON (umbracoNode.id=nuEdited.nodeId AND nuEdited.published=0) -LEFT JOIN cmsContentNu nuPublished ON (umbracoNode.id=nuPublished.nodeId AND nuPublished.published=1) -WHERE umbracoNode.nodeObjectType=@objType -AND nuEdited.nodeId IS NULL OR ({Constants.DatabaseSchema.Tables.Document}.published=1 AND nuPublished.nodeId IS NULL);", - new { objType = contentObjectType }); - - return count == 0; - } - - // assumes media tree lock - public bool VerifyMediaDbCache() - { - // every media item should have a corresponding row for edited properties - Guid mediaObjectType = Constants.ObjectTypes.Media; - - var count = Database.ExecuteScalar( - @"SELECT COUNT(*) -FROM umbracoNode -LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0) -WHERE umbracoNode.nodeObjectType=@objType -AND cmsContentNu.nodeId IS NULL -", - new { objType = mediaObjectType }); - - return count == 0; - } - - // assumes member tree lock - public bool VerifyMemberDbCache() - { - // every member item should have a corresponding row for edited properties - Guid memberObjectType = Constants.ObjectTypes.Member; - - var count = Database.ExecuteScalar( - @"SELECT COUNT(*) -FROM umbracoNode -LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0) -WHERE umbracoNode.nodeObjectType=@objType -AND cmsContentNu.nodeId IS NULL -", - new { objType = memberObjectType }); - - return count == 0; - } - - public ContentNodeKit GetContentSource(int id) - { - Sql? sql = SqlContentSourcesSelect() - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) - .Append(SqlWhereNodeId(SqlContext, id)) - .Append(SqlOrderByLevelIdSortOrder(SqlContext)); - - ContentSourceDto? dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - { - return ContentNodeKit.Empty; - } - - IContentCacheDataSerializer serializer = - _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); - return CreateContentNodeKit(dto, serializer); - } - - public IEnumerable GetAllContentSources() - { - Sql? sql = SqlContentSourcesSelect() - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) - .Append(SqlOrderByLevelIdSortOrder(SqlContext)); - - // Use a more efficient COUNT query - Sql? sqlCountQuery = SqlContentSourcesCount() - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)); - - Sql? sqlCount = - SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); - - IContentCacheDataSerializer serializer = - _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); - - IEnumerable dtos = GetContentNodeDtos(sql, sqlCount); - - foreach (ContentSourceDto row in dtos) - { - yield return CreateContentNodeKit(row, serializer); - } - } - - public IEnumerable GetBranchContentSources(int id) - { - Sql? sql = SqlContentSourcesSelect(SqlContentSourcesSelectUmbracoNodeJoin) - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) - .Append(SqlWhereNodeIdX(SqlContext, id)) - .Append(SqlOrderByLevelIdSortOrder(SqlContext)); - - // Use a more efficient COUNT query - Sql? sqlCountQuery = SqlContentSourcesCount(SqlContentSourcesSelectUmbracoNodeJoin) - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) - .Append(SqlWhereNodeIdX(SqlContext, id)); - - Sql? sqlCount = - SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); - - IContentCacheDataSerializer serializer = - _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); - - IEnumerable dtos = GetContentNodeDtos(sql, sqlCount); - - foreach (ContentSourceDto row in dtos) - { - yield return CreateContentNodeKit(row, serializer); - } - } - - public IEnumerable GetTypeContentSources(IEnumerable? ids) - { - if (!ids?.Any() ?? false) - { - yield break; - } - - Sql? sql = SqlContentSourcesSelect() - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) - .WhereIn(x => x.ContentTypeId, ids) - .Append(SqlOrderByLevelIdSortOrder(SqlContext)); - - // Use a more efficient COUNT query - Sql sqlCountQuery = SqlContentSourcesCount() - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) - .WhereIn(x => x.ContentTypeId, ids); - - Sql? sqlCount = - SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); - - IContentCacheDataSerializer serializer = - _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); - - IEnumerable dtos = GetContentNodeDtos(sql, sqlCount); - - foreach (ContentSourceDto row in dtos) - { - yield return CreateContentNodeKit(row, serializer); - } - } - - public ContentNodeKit GetMediaSource(int id) - { - Sql? sql = SqlMediaSourcesSelect() - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) - .Append(SqlWhereNodeId(SqlContext, id)) - .Append(SqlOrderByLevelIdSortOrder(SqlContext)); - - ContentSourceDto? dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - { - return ContentNodeKit.Empty; - } - - IContentCacheDataSerializer serializer = - _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); - return CreateMediaNodeKit(dto, serializer); - } - - public IEnumerable GetAllMediaSources() - { - Sql? sql = SqlMediaSourcesSelect() - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) - .Append(SqlOrderByLevelIdSortOrder(SqlContext)); - - IContentCacheDataSerializer serializer = - _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); - - IEnumerable dtos = GetMediaNodeDtos(sql); - - foreach (ContentSourceDto row in dtos) - { - yield return CreateMediaNodeKit(row, serializer); - } - } - - public IEnumerable GetBranchMediaSources(int id) - { - Sql? sql = SqlMediaSourcesSelect(SqlContentSourcesSelectUmbracoNodeJoin) - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) - .Append(SqlWhereNodeIdX(SqlContext, id)) - .Append(SqlWhereNodeIdX(SqlContext, id)) - .Append(SqlOrderByLevelIdSortOrder(SqlContext)); - - IContentCacheDataSerializer serializer = - _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); - - IEnumerable dtos = GetMediaNodeDtos(sql); - - foreach (ContentSourceDto row in dtos) - { - yield return CreateMediaNodeKit(row, serializer); - } - } - - public IEnumerable GetTypeMediaSources(IEnumerable ids) - { - if (!ids.Any()) - { - yield break; - } - - Sql? sql = SqlMediaSourcesSelect() - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) - .WhereIn(x => x.ContentTypeId, ids) - .Append(SqlOrderByLevelIdSortOrder(SqlContext)); - - IContentCacheDataSerializer serializer = - _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); - - IEnumerable dtos = GetMediaNodeDtos(sql); - - foreach (ContentSourceDto row in dtos) - { - yield return CreateMediaNodeKit(row, serializer); - } - } - - public ContentNodeKit GetMediaSource(IScope scope, int id) - { - Sql? sql = SqlMediaSourcesSelect() - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) - .Append(SqlWhereNodeId(SqlContext, id)) - .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext)); - - ContentSourceDto? dto = scope.Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - { - return ContentNodeKit.Empty; - } - - IContentCacheDataSerializer serializer = - _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); - return CreateMediaNodeKit(dto, serializer); - } - - private void OnRepositoryRefreshed(IContentCacheDataSerializer serializer, IContentBase content, bool published) - { - // use a custom SQL to update row version on each update - // db.InsertOrUpdate(dto); - ContentNuDto dto = GetDto(content, published, serializer); - - Database.InsertOrUpdate( - dto, - "SET data=@data, dataRaw=@dataRaw, rv=rv+1 WHERE nodeId=@id AND published=@published", - new - { - dataRaw = dto.RawData ?? Array.Empty(), - data = dto.Data, - id = dto.NodeId, - published = dto.Published, - }); - } - - // assumes content tree lock - private void RebuildContentDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection? contentTypeIds) - { - Guid contentObjectType = Constants.ObjectTypes.Document; - - // remove all - if anything fails the transaction will rollback - if (contentTypeIds == null || contentTypeIds.Count == 0) - { - // must support SQL-CE - Database.Execute( - @"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType -)", - new { objType = contentObjectType }); - } - else - { - // assume number of ctypes won't blow IN(...) - // must support SQL-CE - Database.Execute( - $@"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode - JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id - WHERE umbracoNode.nodeObjectType=@objType - AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) -)", - new { objType = contentObjectType, ctypes = contentTypeIds }); - } - - // insert back - if anything fails the transaction will rollback - IQuery query = SqlContext.Query(); - if (contentTypeIds != null && contentTypeIds.Count > 0) - { - query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...) - } - - long pageIndex = 0; - long processed = 0; - long total; - do - { - // the tree is locked, counting and comparing to total is safe - IEnumerable descendants = - _documentRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - var items = new List(); - var count = 0; - foreach (IContent c in descendants) - { - // always the edited version - items.Add(GetDto(c, false, serializer)); - - // and also the published version if it makes any sense - if (c.Published) - { - items.Add(GetDto(c, true, serializer)); - } - - count++; - } - - Database.BulkInsertRecords(items); - processed += count; - } - while (processed < total); - } - - // assumes media tree lock - private void RebuildMediaDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection? contentTypeIds) - { - Guid mediaObjectType = Constants.ObjectTypes.Media; - - // remove all - if anything fails the transaction will rollback - if (contentTypeIds is null || contentTypeIds.Count == 0) - { - // must support SQL-CE - Database.Execute( - @"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType -)", - new { objType = mediaObjectType }); - } - else - { - // assume number of ctypes won't blow IN(...) - // must support SQL-CE - Database.Execute( - $@"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode - JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id - WHERE umbracoNode.nodeObjectType=@objType - AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) -)", - new { objType = mediaObjectType, ctypes = contentTypeIds }); - } - - // insert back - if anything fails the transaction will rollback - IQuery query = SqlContext.Query(); - if (contentTypeIds is not null && contentTypeIds.Count > 0) - { - query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...) - } - - long pageIndex = 0; - long processed = 0; - long total; - do - { - // the tree is locked, counting and comparing to total is safe - IEnumerable descendants = - _mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - var items = descendants.Select(m => GetDto(m, false, serializer)).ToArray(); - Database.BulkInsertRecords(items); - processed += items.Length; - } - while (processed < total); - } - - // assumes member tree lock - private void RebuildMemberDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection? contentTypeIds) - { - Guid memberObjectType = Constants.ObjectTypes.Member; - - // remove all - if anything fails the transaction will rollback - if (contentTypeIds == null || contentTypeIds.Count == 0) - { - // must support SQL-CE - Database.Execute( - @"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType -)", - new { objType = memberObjectType }); - } - else - { - // assume number of ctypes won't blow IN(...) - // must support SQL-CE - Database.Execute( - $@"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode - JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id - WHERE umbracoNode.nodeObjectType=@objType - AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) -)", - new { objType = memberObjectType, ctypes = contentTypeIds }); - } - - // insert back - if anything fails the transaction will rollback - IQuery query = SqlContext.Query(); - if (contentTypeIds != null && contentTypeIds.Count > 0) - { - query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...) - } - - long pageIndex = 0; - long processed = 0; - long total; - do - { - IEnumerable descendants = - _memberRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - ContentNuDto[] items = descendants.Select(m => GetDto(m, false, serializer)).ToArray(); - Database.BulkInsertRecords(items); - processed += items.Length; - } - while (processed < total); - } - - private ContentNuDto GetDto(IContentBase content, bool published, IContentCacheDataSerializer serializer) - { - // should inject these in ctor - // BUT for the time being we decide not to support ConvertDbToXml/String - // var propertyEditorResolver = PropertyEditorResolver.Current; - // var dataTypeService = ApplicationContext.Current.Services.DataTypeService; - var propertyData = new Dictionary(); - foreach (IProperty prop in content.Properties) - { - var pdatas = new List(); - foreach (IPropertyValue pvalue in prop.Values.OrderBy(x => x.Culture)) - { - // sanitize - properties should be ok but ... never knows - if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) - { - continue; - } - - // note: at service level, invariant is 'null', but here invariant becomes 'string.Empty' - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - if (value != null) - { - pdatas.Add(new PropertyData - { - Culture = pvalue.Culture ?? string.Empty, - Segment = pvalue.Segment ?? string.Empty, - Value = value, - }); - } - } - - propertyData[prop.Alias] = pdatas.ToArray(); - } - - var cultureData = new Dictionary(); - - // sanitize - names should be ok but ... never knows - if (content.ContentType.VariesByCulture()) - { - ContentCultureInfosCollection? infos = content is IContent document - ? published - ? document.PublishCultureInfos - : document.CultureInfos - : content.CultureInfos; - - // ReSharper disable once UseDeconstruction - if (infos is not null) - { - foreach (ContentCultureInfos cultureInfo in infos) - { - var cultureIsDraft = !published && content is IContent d && d.IsCultureEdited(cultureInfo.Culture); - cultureData[cultureInfo.Culture] = new CultureVariation - { - Name = cultureInfo.Name, - UrlSegment = - content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, cultureInfo.Culture), - Date = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue, - IsDraft = cultureIsDraft, - }; - } - } - } - - // the dictionary that will be serialized - var contentCacheData = new ContentCacheDataModel - { - PropertyData = propertyData, - CultureData = cultureData, - UrlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders), - }; - - ContentCacheDataSerializationResult serialized = - serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData, published); - - var dto = new ContentNuDto - { - NodeId = content.Id, - Published = published, - Data = serialized.StringData, - RawData = serialized.ByteData, - }; - - return dto; - } - - // we want arrays, we want them all loaded, not an enumerable - private Sql SqlContentSourcesSelect(Func>? joins = null) - { - SqlTemplate sqlTemplate = SqlContext.Templates.Get( - Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesSelect, - tsql => - tsql.Select( - x => Alias(x.NodeId, "Id"), - x => Alias(x.UniqueId, "Key"), - x => Alias(x.Level, "Level"), - x => Alias(x.Path, "Path"), - x => Alias(x.SortOrder, "SortOrder"), - x => Alias(x.ParentId, "ParentId"), - x => Alias(x.CreateDate, "CreateDate"), - x => Alias(x.UserId, "CreatorId")) - .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) - .AndSelect(x => Alias(x.Published, "Published"), x => Alias(x.Edited, "Edited")) - .AndSelect( - x => Alias(x.Id, "VersionId"), - x => Alias(x.Text, "EditName"), - x => Alias(x.VersionDate, "EditVersionDate"), - x => Alias(x.UserId, "EditWriterId")) - .AndSelect(x => Alias(x.TemplateId, "EditTemplateId")) - .AndSelect( - "pcver", - x => Alias(x.Id, "PublishedVersionId"), - x => Alias(x.Text, "PubName"), - x => Alias(x.VersionDate, "PubVersionDate"), - x => Alias(x.UserId, "PubWriterId")) - .AndSelect("pdver", x => Alias(x.TemplateId, "PubTemplateId")) - .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) - .AndSelect("nuPub", x => Alias(x.Data, "PubData")) - .AndSelect("nuEdit", x => Alias(x.RawData, "EditDataRaw")) - .AndSelect("nuPub", x => Alias(x.RawData, "PubDataRaw")) - .From()); - - Sql? sql = sqlTemplate.Sql(); - - // TODO: I'm unsure how we can format the below into SQL templates also because right.Current and right.Published end up being parameters - if (joins != null) - { - sql = sql.Append(joins(sql.SqlContext)); - } - - sql = sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin() - .On((left, right) => left.NodeId == right.NodeId && right.Current) - .InnerJoin() - .On((left, right) => left.Id == right.Id) - .LeftJoin( - j => - j.InnerJoin("pdver") - .On( - (left, right) => left.Id == right.Id && right.Published == true, "pcver", "pdver"), - "pcver") - .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcver") - .LeftJoin("nuEdit").On( - (left, right) => left.NodeId == right.NodeId && right.Published == false, aliasRight: "nuEdit") - .LeftJoin("nuPub").On( - (left, right) => left.NodeId == right.NodeId && right.Published == true, aliasRight: "nuPub"); - - return sql; - } - - private Sql SqlContentSourcesSelectUmbracoNodeJoin(ISqlContext sqlContext) - { - ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; - - SqlTemplate sqlTemplate = sqlContext.Templates.Get( - Constants.SqlTemplates.NuCacheDatabaseDataSource.SourcesSelectUmbracoNodeJoin, builder => - builder.InnerJoin("x") - .On( - (left, right) => left.NodeId == right.NodeId || - SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), - aliasRight: "x")); - - Sql sql = sqlTemplate.Sql(); - return sql; - } - - private Sql SqlWhereNodeId(ISqlContext sqlContext, int id) - { - ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; - - SqlTemplate sqlTemplate = sqlContext.Templates.Get( - Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeId, - builder => - builder.Where(x => x.NodeId == SqlTemplate.Arg("id"))); - - Sql sql = sqlTemplate.Sql(id); - return sql; - } - - private Sql SqlWhereNodeIdX(ISqlContext sqlContext, int id) - { - ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; - - SqlTemplate sqlTemplate = sqlContext.Templates.Get( - Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeIdX, s => - s.Where(x => x.NodeId == SqlTemplate.Arg("id"), "x")); - - Sql sql = sqlTemplate.Sql(id); - return sql; - } - - private Sql SqlOrderByLevelIdSortOrder(ISqlContext sqlContext) - { - ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; - - SqlTemplate sqlTemplate = sqlContext.Templates.Get( - Constants.SqlTemplates.NuCacheDatabaseDataSource.OrderByLevelIdSortOrder, s => - s.OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder)); - - Sql sql = sqlTemplate.Sql(); - return sql; - } - - private Sql SqlObjectTypeNotTrashed(ISqlContext sqlContext, Guid nodeObjectType) - { - ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; - - SqlTemplate sqlTemplate = sqlContext.Templates.Get( - Constants.SqlTemplates.NuCacheDatabaseDataSource.ObjectTypeNotTrashedFilter, s => - s.Where(x => - x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && - x.Trashed == SqlTemplate.Arg("trashed"))); - - Sql sql = sqlTemplate.Sql(nodeObjectType, false); - return sql; - } - - /// - /// Returns a slightly more optimized query to use for the document counting when paging over the content sources - /// - /// - /// - private Sql SqlContentSourcesCount(Func>? joins = null) - { - SqlTemplate sqlTemplate = SqlContext.Templates.Get( - Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesCount, tsql => - tsql.Select(x => Alias(x.NodeId, "Id")) - .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId)); - - Sql? sql = sqlTemplate.Sql(); - - if (joins != null) - { - sql = sql.Append(joins(sql.SqlContext)); - } - - // TODO: We can't use a template with this one because of the 'right.Current' and 'right.Published' ends up being a parameter so not sure how we can do that - sql = sql - .InnerJoin() - .On((left, right) => left.NodeId == right.NodeId && right.Current) - .InnerJoin() - .On((left, right) => left.Id == right.Id) - .LeftJoin( - j => - j.InnerJoin("pdver") - .On( - (left, right) => left.Id == right.Id && right.Published, - "pcver", - "pdver"), - "pcver") - .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcver"); - - return sql; - } - - private Sql SqlMediaSourcesSelect(Func>? joins = null) - { - SqlTemplate sqlTemplate = SqlContext.Templates.Get( - Constants.SqlTemplates.NuCacheDatabaseDataSource.MediaSourcesSelect, tsql => - tsql.Select( - x => Alias(x.NodeId, "Id"), - x => Alias(x.UniqueId, "Key"), - x => Alias(x.Level, "Level"), - x => Alias(x.Path, "Path"), - x => Alias(x.SortOrder, "SortOrder"), - x => Alias(x.ParentId, "ParentId"), - x => Alias(x.CreateDate, "CreateDate"), - x => Alias(x.UserId, "CreatorId")) - .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) - .AndSelect( - x => Alias(x.Id, "VersionId"), - x => Alias(x.Text, "EditName"), - x => Alias(x.VersionDate, "EditVersionDate"), - x => Alias(x.UserId, "EditWriterId")) - .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) - .AndSelect("nuEdit", x => Alias(x.RawData, "EditDataRaw")) - .From()); - - Sql? sql = sqlTemplate.Sql(); - - if (joins != null) - { - sql = sql.Append(joins(sql.SqlContext)); - } - - // TODO: We can't use a template with this one because of the 'right.Published' ends up being a parameter so not sure how we can do that - sql = sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin() - .On((left, right) => left.NodeId == right.NodeId && right.Current) - .LeftJoin("nuEdit") - .On( - (left, right) => left.NodeId == right.NodeId && !right.Published, - aliasRight: "nuEdit"); - - return sql; - } - - private Sql SqlMediaSourcesCount(Func>? joins = null) - { - SqlTemplate sqlTemplate = SqlContext.Templates.Get( - Constants.SqlTemplates.NuCacheDatabaseDataSource.MediaSourcesCount, tsql => - tsql.Select(x => Alias(x.NodeId, "Id")).From()); - - Sql? sql = sqlTemplate.Sql(); - - if (joins != null) - { - sql = sql.Append(joins(sql.SqlContext)); - } - - // TODO: We can't use a template with this one because of the 'right.Current' ends up being a parameter so not sure how we can do that - sql = sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin() - .On((left, right) => left.NodeId == right.NodeId && right.Current); - - return sql; - } - - private ContentNodeKit CreateContentNodeKit(ContentSourceDto dto, IContentCacheDataSerializer serializer) - { - ContentData? d = null; - ContentData? p = null; - - if (dto.Edited) - { - if (dto.EditData == null && dto.EditDataRaw == null) - { - if (Debugger.IsAttached) - { - throw new InvalidOperationException("Missing cmsContentNu edited content for node " + dto.Id + - ", consider rebuilding."); - } - - _logger.LogWarning( - "Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", - dto.Id); - } - else - { - var published = false; - ContentCacheDataModel? deserializedContent = - serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published); - - d = new ContentData( - dto.EditName, - deserializedContent?.UrlSegment, - dto.VersionId, - dto.EditVersionDate, - dto.EditWriterId, - dto.EditTemplateId == 0 ? null : dto.EditTemplateId, - published, - deserializedContent?.PropertyData, - deserializedContent?.CultureData); - } - } - - if (dto.Published) - { - if (dto.PubData == null && dto.PubDataRaw == null) - { - if (Debugger.IsAttached) - { - throw new InvalidOperationException("Missing cmsContentNu published content for node " + dto.Id + - ", consider rebuilding."); - } - - _logger.LogWarning( - "Missing cmsContentNu published content for node {NodeId}, consider rebuilding.", - dto.Id); - } - else - { - var published = true; - ContentCacheDataModel? deserializedContent = - serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw, published); - - p = new ContentData( - dto.PubName, - deserializedContent?.UrlSegment, - dto.VersionId, - dto.PubVersionDate, - dto.PubWriterId, - dto.PubTemplateId == 0 ? null : dto.PubTemplateId, - published, - deserializedContent?.PropertyData, - deserializedContent?.CultureData); - } - } - - var n = new ContentNode(dto.Id, dto.Key, dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId); - - var s = new ContentNodeKit(n, dto.ContentTypeId, d, p); - - return s; - } - - private ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto, IContentCacheDataSerializer serializer) - { - if (dto.EditData == null && dto.EditDataRaw == null) - { - throw new InvalidOperationException("No data for media " + dto.Id); - } - - var published = true; - ContentCacheDataModel? deserializedMedia = - serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published); - - var p = new ContentData( - dto.EditName, - null, - dto.VersionId, - dto.EditVersionDate, - dto.CreatorId, - -1, - published, - deserializedMedia?.PropertyData, - deserializedMedia?.CultureData); - - var n = new ContentNode(dto.Id, dto.Key, dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId); - - var s = new ContentNodeKit(n, dto.ContentTypeId, null, p); - - return s; - } - - private IEnumerable GetMediaNodeDtos(Sql sql) - { - // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. - // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - // QueryPaged is very slow on large sites however, so use fetch if UsePagedSqlQuery is disabled. - IEnumerable dtos; - if (_nucacheSettings.Value.UsePagedSqlQuery) - { - // Use a more efficient COUNT query - Sql? sqlCountQuery = SqlMediaSourcesCount() - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)); - - Sql? sqlCount = - SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); - - dtos = Database.QueryPaged(_nucacheSettings.Value.SqlPageSize, sql, sqlCount); - } - else - { - dtos = Database.Fetch(sql); - } - - return dtos; - } - - private IEnumerable GetContentNodeDtos(Sql sql, Sql sqlCount) - { - // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. - // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - // QueryPaged is very slow on large sites however, so use fetch if UsePagedSqlQuery is disabled. - IEnumerable dtos = _nucacheSettings.Value.UsePagedSqlQuery ? - Database.QueryPaged(_nucacheSettings.Value.SqlPageSize, sql, sqlCount) : - Database.Fetch(sql); - - return dtos; - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs deleted file mode 100644 index 13e911f137..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs +++ /dev/null @@ -1,183 +0,0 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Logging; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Core.Services; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence; - -public class NuCacheContentService : RepositoryService, INuCacheContentService -{ - private const string NuCacheSerializerKey = "Umbraco.Web.PublishedCache.NuCache.Serializer"; - private readonly IKeyValueService _keyValueService; - private readonly ILogger _logger; - private readonly IOptions _nucacheSettings; - private readonly IProfilingLogger _profilingLogger; - private readonly INuCacheContentRepository _repository; - - public NuCacheContentService( - INuCacheContentRepository repository, - IKeyValueService keyValueService, - ICoreScopeProvider provider, - ILoggerFactory loggerFactory, - IProfilingLogger profilingLogger, - IEventMessagesFactory eventMessagesFactory, - IOptions nucacheSettings) - : base(provider, loggerFactory, eventMessagesFactory) - { - _repository = repository; - _keyValueService = keyValueService; - _profilingLogger = profilingLogger; - _nucacheSettings = nucacheSettings; - _logger = loggerFactory.CreateLogger(); - } - - public void RebuildDatabaseCacheIfSerializerChanged() - { - NuCacheSerializerType serializer = _nucacheSettings.Value.NuCacheSerializerType; - var currentSerializerValue = _keyValueService.GetValue(NuCacheSerializerKey); - - if (!Enum.TryParse(currentSerializerValue, out NuCacheSerializerType currentSerializer) - || serializer != currentSerializer) - { - _logger.LogWarning( - "Database NuCache was serialized using {CurrentSerializer}. Currently configured NuCache serializer {Serializer}. Rebuilding Nucache", - currentSerializer, serializer); - - using (_profilingLogger.TraceDuration( - $"Rebuilding NuCache database with {serializer} serializer")) - { - RebuildAll(); - _keyValueService.SetValue(NuCacheSerializerKey, serializer.ToString()); - } - } - } - - /// - public IEnumerable GetAllContentSources() - => _repository.GetAllContentSources(); - - /// - public IEnumerable GetAllMediaSources() - => _repository.GetAllMediaSources(); - - /// - public IEnumerable GetBranchContentSources(int id) - => _repository.GetBranchContentSources(id); - - /// - public IEnumerable GetBranchMediaSources(int id) - => _repository.GetBranchMediaSources(id); - - /// - public ContentNodeKit GetContentSource(int id) - => _repository.GetContentSource(id); - - /// - public ContentNodeKit GetMediaSource(int id) - => _repository.GetMediaSource(id); - - /// - public IEnumerable GetTypeContentSources(IEnumerable? ids) - => _repository.GetTypeContentSources(ids); - - /// - public IEnumerable GetTypeMediaSources(IEnumerable ids) - => _repository.GetTypeContentSources(ids); - - /// - public void DeleteContentItem(IContentBase item) - => _repository.DeleteContentItem(item); - - public void DeleteContentItems(IEnumerable items) - { - foreach (IContentBase item in items) - { - _repository.DeleteContentItem(item); - } - } - - /// - public void RefreshContent(IContent content) - => _repository.RefreshContent(content); - - /// - public void RefreshMedia(IMedia media) - => _repository.RefreshMedia(media); - - /// - public void RefreshMember(IMember member) - => _repository.RefreshMember(member); - - /// - public void RebuildAll() - { - Rebuild(Array.Empty(), Array.Empty(), Array.Empty()); - } - - /// - public void Rebuild( - IReadOnlyCollection? contentTypeIds = null, - IReadOnlyCollection? mediaTypeIds = null, - IReadOnlyCollection? memberTypeIds = null) - { - using (ICoreScope scope = ScopeProvider.CreateCoreScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) - { - if (contentTypeIds is null && mediaTypeIds is null && memberTypeIds is null) - { - scope.ReadLock(Constants.Locks.ContentTree,Constants.Locks.MediaTree,Constants.Locks.MemberTree); - } - - if (contentTypeIds is not null && contentTypeIds.Any()) - { - scope.ReadLock(Constants.Locks.ContentTree); - } - - if (mediaTypeIds is not null && mediaTypeIds.Any()) - { - scope.ReadLock(Constants.Locks.MediaTree); - } - - if (memberTypeIds is not null && memberTypeIds.Any()) - { - scope.ReadLock(Constants.Locks.MemberTree); - } - - _repository.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds); - - // Save a key/value of the serialized type. This is used during startup to see - // if the serialized type changed and if so it will rebuild with the correct type. - _keyValueService.SetValue(NuCacheSerializerKey, _nucacheSettings.Value.NuCacheSerializerType.ToString()); - - scope.Complete(); - } - } - - /// - public bool VerifyContentDbCache() - { - using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); - scope.ReadLock(Constants.Locks.ContentTree); - return _repository.VerifyContentDbCache(); - } - - /// - public bool VerifyMediaDbCache() - { - using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); - scope.ReadLock(Constants.Locks.MediaTree); - return _repository.VerifyMediaDbCache(); - } - - /// - public bool VerifyMemberDbCache() - { - using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); - scope.ReadLock(Constants.Locks.MemberTree); - return _repository.VerifyMemberDbCache(); - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/Property.cs b/src/Umbraco.PublishedCache.NuCache/Property.cs deleted file mode 100644 index 845b8312c5..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Property.cs +++ /dev/null @@ -1,423 +0,0 @@ -using System.Collections.Concurrent; -using System.Xml.Serialization; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Collections; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -[Serializable] -[XmlType(Namespace = "http://umbraco.org/webservices/")] -internal class Property : PublishedPropertyBase -{ - private readonly PublishedContent _content; - private readonly Guid _contentUid; - private readonly bool _isMember; - private readonly bool _isPreviewing; - - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - - // the invariant-neutral source and inter values - private readonly object? _sourceValue; - private readonly ContentVariation _variations; - private readonly ContentVariation _sourceVariations; - - // the variant and non-variant object values - private CacheValues? _cacheValues; - private bool _interInitialized; - private object? _interValue; - - // the variant source and inter values - private readonly object _locko = new(); - private ConcurrentDictionary? _sourceValues; - - private string? _valuesCacheKey; - - // initializes a published content property with no value - public Property( - IPublishedPropertyType propertyType, - PublishedContent content, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element) - : this(propertyType, content, null, publishedSnapshotAccessor, referenceCacheLevel) - { - } - - // initializes a published content property with a value - public Property( - IPublishedPropertyType propertyType, - PublishedContent content, - PropertyData[]? sourceValues, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element) - : base(propertyType, referenceCacheLevel) - { - if (sourceValues != null) - { - foreach (PropertyData sourceValue in sourceValues) - { - if (sourceValue.Culture == string.Empty && sourceValue.Segment == string.Empty) - { - _sourceValue = sourceValue.Value; - } - else - { - EnsureSourceValuesInitialized(); - - _sourceValues![new CompositeStringStringKey(sourceValue.Culture, sourceValue.Segment)] - = new SourceInterValue - { - Culture = sourceValue.Culture, - Segment = sourceValue.Segment, - SourceValue = sourceValue.Value, - }; - } - } - } - - _contentUid = content.Key; - _content = content; - _isPreviewing = content.IsPreviewing; - _isMember = content.ContentType.ItemType == PublishedItemType.Member; - _publishedSnapshotAccessor = publishedSnapshotAccessor; - // this variable is used for contextualizing the variation level when calculating property values. - // it must be set to the union of variance (the combination of content type and property type variance). - _variations = propertyType.Variations | content.ContentType.Variations; - _sourceVariations = propertyType.Variations; - } - - // clone for previewing as draft a published content that is published and has no draft - public Property(Property origin, PublishedContent content) - : base(origin.PropertyType, origin.ReferenceCacheLevel) - { - _sourceValue = origin._sourceValue; - _sourceValues = origin._sourceValues; - - _contentUid = origin._contentUid; - _content = content; - _isPreviewing = true; - _isMember = origin._isMember; - _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; - _variations = origin._variations; - _sourceVariations = origin._sourceVariations; - } - - // used to cache the CacheValues of this property - internal string ValuesCacheKey => _valuesCacheKey ??= - CacheKeys.PropertyCacheValues(_contentUid, Alias, _isPreviewing); - - // determines whether a property has value - public override bool HasValue(string? culture = null, string? segment = null) - { - _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); - - var value = GetSourceValue(culture, segment); - var hasValue = PropertyType.IsValue(value, PropertyValueLevel.Source); - if (hasValue.HasValue) - { - return hasValue.Value; - } - - value = GetInterValue(culture, segment); - hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); - if (hasValue.HasValue) - { - return hasValue.Value; - } - - CacheValue cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); - - // initial reference cache level always is .Content - const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; - - if (!cacheValues.ObjectInitialized) - { - cacheValues.ObjectValue = - PropertyType.ConvertInterToObject(_content, initialCacheLevel, value, _isPreviewing); - cacheValues.ObjectInitialized = true; - } - - value = cacheValues.ObjectValue; - return PropertyType.IsValue(value, PropertyValueLevel.Object) ?? false; - } - - public override object? GetSourceValue(string? culture = null, string? segment = null) - { - _content.VariationContextAccessor.ContextualizeVariation(_sourceVariations, _content.Id, ref culture, ref segment); - - // source values are tightly bound to the property/schema culture and segment configurations, so we need to - // sanitize the contextualized culture/segment states before using them to access the source values. - culture = _sourceVariations.VariesByCulture() ? culture : string.Empty; - segment = _sourceVariations.VariesBySegment() ? segment : string.Empty; - - if (culture == string.Empty && segment == string.Empty) - { - return _sourceValue; - } - - if (_sourceValues == null) - { - return null; - } - - return _sourceValues.TryGetValue( - new CompositeStringStringKey(culture, segment), - out SourceInterValue? sourceValue) - ? sourceValue.SourceValue - : null; - } - - private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) - { - CacheValues cacheValues; - IPublishedSnapshot publishedSnapshot; - IAppCache? cache; - switch (cacheLevel) - { - case PropertyCacheLevel.None: - // never cache anything - cacheValues = new CacheValues(); - break; - case PropertyCacheLevel.Element: - // cache within the property object itself, ie within the content object - cacheValues = _cacheValues ??= new CacheValues(); - break; - case PropertyCacheLevel.Elements: - // cache within the elements cache, unless previewing, then use the snapshot or - // elements cache (if we don't want to pollute the elements cache with short-lived - // data) depending on settings - // for members, always cache in the snapshot cache - never pollute elements cache - publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - cache = publishedSnapshot == null - ? null - : (_isPreviewing == false || PublishedSnapshotService.FullCacheWhenPreviewing) && _isMember == false - ? publishedSnapshot.ElementsCache - : publishedSnapshot.SnapshotCache; - cacheValues = GetCacheValues(cache); - break; - case PropertyCacheLevel.Snapshot: - // cache within the snapshot cache - publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - cache = publishedSnapshot?.SnapshotCache; - cacheValues = GetCacheValues(cache); - break; - default: - throw new InvalidOperationException("Invalid cache level."); - } - - return cacheValues; - } - - private CacheValues GetCacheValues(IAppCache? cache) - { - // no cache, don't cache - if (cache == null) - { - return new CacheValues(); - } - - return (CacheValues)cache.Get(ValuesCacheKey, () => new CacheValues())!; - } - - private object? GetInterValue(string? culture, string? segment) - { - if (culture == string.Empty && segment == string.Empty) - { - if (_interInitialized) - { - return _interValue; - } - - _interValue = PropertyType.ConvertSourceToInter(_content, _sourceValue, _isPreviewing); - _interInitialized = true; - return _interValue; - } - - EnsureSourceValuesInitialized(); - - var k = new CompositeStringStringKey(culture, segment); - - SourceInterValue vvalue = _sourceValues!.GetOrAdd(k, _ => - new SourceInterValue - { - Culture = culture, - Segment = segment, - SourceValue = GetSourceValue(culture, segment), - }); - - if (vvalue.InterInitialized) - { - return vvalue.InterValue; - } - - vvalue.InterValue = PropertyType.ConvertSourceToInter(_content, vvalue.SourceValue, _isPreviewing); - vvalue.InterInitialized = true; - return vvalue.InterValue; - } - - public override object? GetValue(string? culture = null, string? segment = null) - { - _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); - - object? value; - CacheValue cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); - - // initial reference cache level always is .Content - const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; - - if (cacheValues.ObjectInitialized) - { - return cacheValues.ObjectValue; - } - - cacheValues.ObjectValue = PropertyType.ConvertInterToObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing); - cacheValues.ObjectInitialized = true; - value = cacheValues.ObjectValue; - - return value; - } - - public override object? GetDeliveryApiValue(bool expanding, string? culture = null, string? segment = null) - { - _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); - - object? value; - CacheValue cacheValues = GetCacheValues(expanding ? PropertyType.DeliveryApiCacheLevelForExpansion : PropertyType.DeliveryApiCacheLevel).For(culture, segment); - - - // initial reference cache level always is .Content - const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; - - object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing, expanding); - value = expanding - ? GetDeliveryApiExpandedObject(cacheValues, GetDeliveryApiObject) - : GetDeliveryApiDefaultObject(cacheValues, GetDeliveryApiObject); - - return value; - } - - private object? GetDeliveryApiDefaultObject(CacheValue cacheValues, Func getValue) - { - if (cacheValues.DeliveryApiDefaultObjectInitialized == false) - { - cacheValues.DeliveryApiDefaultObjectValue = getValue(); - cacheValues.DeliveryApiDefaultObjectInitialized = true; - } - - return cacheValues.DeliveryApiDefaultObjectValue; - } - - private object? GetDeliveryApiExpandedObject(CacheValue cacheValues, Func getValue) - { - if (cacheValues.DeliveryApiExpandedObjectInitialized == false) - { - cacheValues.DeliveryApiExpandedObjectValue = getValue(); - cacheValues.DeliveryApiExpandedObjectInitialized = true; - } - - return cacheValues.DeliveryApiExpandedObjectValue; - } - - #region Classes - - private class CacheValue - { - public bool ObjectInitialized { get; set; } - - public object? ObjectValue { get; set; } - - public bool XPathInitialized { get; set; } - - public object? XPathValue { get; set; } - - public bool DeliveryApiDefaultObjectInitialized { get; set; } - - public object? DeliveryApiDefaultObjectValue { get; set; } - - public bool DeliveryApiExpandedObjectInitialized { get; set; } - - public object? DeliveryApiExpandedObjectValue { get; set; } - } - - private class CacheValues : CacheValue - { - private readonly object _locko = new(); - private ConcurrentDictionary? _values; - - public CacheValue For(string? culture, string? segment) - { - // As noted on IPropertyValue, null value means invariant - // But as we need an actual string value to build a CompositeStringStringKey - // We need to convert null to empty - culture ??= string.Empty; - segment ??= string.Empty; - - if (culture == string.Empty && segment == string.Empty) - { - return this; - } - - if (_values == null) - { - lock (_locko) - { - _values ??= InitializeConcurrentDictionary(); - } - } - - var k = new CompositeStringStringKey(culture, segment); - - CacheValue value = _values.GetOrAdd(k, _ => new CacheValue()); - - return value; - } - } - - private class SourceInterValue - { - private string? _culture; - private string? _segment; - - public string? Culture - { - get => _culture; - internal set => _culture = value?.ToLowerInvariant(); - } - - public string? Segment - { - get => _segment; - internal set => _segment = value?.ToLowerInvariant(); - } - - public object? SourceValue { get; set; } - - public bool InterInitialized { get; set; } - - public object? InterValue { get; set; } - } - - private static ConcurrentDictionary InitializeConcurrentDictionary() - where TKey : notnull - => new(-1, 5); - - private void EnsureSourceValuesInitialized() - { - if (_sourceValues is not null) - { - return; - } - - lock (_locko) - { - _sourceValues ??= InitializeConcurrentDictionary(); - } - } - - #endregion -} diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs b/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs deleted file mode 100644 index f84df0644d..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs +++ /dev/null @@ -1,438 +0,0 @@ -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Exceptions; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -internal class PublishedContent : PublishedContentBase -{ - private readonly ContentNode _contentNode; - private readonly IPublishedModelFactory? _publishedModelFactory; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly string? _urlSegment; - - #region Content Type - - /// - public override IPublishedContentType ContentType => _contentNode.ContentType; - - #endregion - - #region PublishedElement - - /// - public override Guid Key => _contentNode.Uid; - - #endregion - - #region Constructors - - public PublishedContent( - ContentNode contentNode, - ContentData contentData, - IPublishedSnapshotAccessor? publishedSnapshotAccessor, - IVariationContextAccessor? variationContextAccessor, - IPublishedModelFactory? publishedModelFactory) - : base(variationContextAccessor) - { - _contentNode = contentNode ?? throw new ArgumentNullException(nameof(contentNode)); - ContentData = contentData ?? throw new ArgumentNullException(nameof(contentData)); - _publishedSnapshotAccessor = publishedSnapshotAccessor ?? - throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); - _publishedModelFactory = publishedModelFactory; - VariationContextAccessor = variationContextAccessor ?? - throw new ArgumentNullException(nameof(variationContextAccessor)); - - _urlSegment = ContentData.UrlSegment; - IsPreviewing = ContentData.Published == false; - - var properties = new IPublishedProperty[_contentNode.ContentType.PropertyTypes.Count()]; - var i = 0; - foreach (IPublishedPropertyType propertyType in _contentNode.ContentType.PropertyTypes) - { - // add one property per property type - this is required, for the indexing to work - // if contentData supplies pdatas, use them, else use null - contentData.Properties.TryGetValue(propertyType.Alias, out PropertyData[]? pdatas); // else will be null - properties[i++] = new Property(propertyType, this, pdatas, _publishedSnapshotAccessor, propertyType.CacheLevel); - } - - PropertiesArray = properties; - } - - // used when cloning in ContentNode - public PublishedContent( - ContentNode contentNode, - PublishedContent origin, - IVariationContextAccessor variationContextAccessor) - : base(variationContextAccessor) - { - _contentNode = contentNode; - _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; - VariationContextAccessor = origin.VariationContextAccessor; - ContentData = origin.ContentData; - - _urlSegment = origin._urlSegment; - IsPreviewing = origin.IsPreviewing; - - // here is the main benefit: we do not re-create properties so if anything - // is cached locally, we share the cache - which is fine - if anything depends - // on the tree structure, it should not be cached locally to begin with - PropertiesArray = origin.PropertiesArray; - } - - // clone for previewing as draft a published content that is published and has no draft - private PublishedContent(PublishedContent origin) - : base(origin.VariationContextAccessor) - { - _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; - VariationContextAccessor = origin.VariationContextAccessor; - _contentNode = origin._contentNode; - ContentData = origin.ContentData; - - _urlSegment = origin._urlSegment; - IsPreviewing = true; - - // clone properties so _isPreviewing is true - PropertiesArray = origin.PropertiesArray.Select(x => (IPublishedProperty)new Property((Property)x, this)) - .ToArray(); - } - - #endregion - - #region Get Content/Media for Parent/Children - - // this is for tests purposes - // args are: current published snapshot (may be null), previewing, content id - returns: content - internal static Func GetContentByIdFunc { get; set; } - = (publishedShapshot, previewing, id) => publishedShapshot.Content?.GetById(previewing, id); - - internal static Func GetMediaByIdFunc { get; set; } - = (publishedShapshot, previewing, id) => publishedShapshot.Media?.GetById(previewing, id); - - private Func GetGetterById() - { - switch (ContentType.ItemType) - { - case PublishedItemType.Content: - return GetContentByIdFunc; - case PublishedItemType.Media: - return GetMediaByIdFunc; - default: - throw new PanicException("invalid item type"); - } - } - - #endregion - - #region PublishedContent - - internal ContentData ContentData { get; } - - /// - public override int Id => _contentNode.Id; - - /// - public override int SortOrder => _contentNode.SortOrder; - - /// - public override int Level => _contentNode.Level; - - /// - public override string Path => _contentNode.Path; - - /// - public override int? TemplateId => ContentData.TemplateId; - - /// - public override int CreatorId => _contentNode.CreatorId; - - /// - public override DateTime CreateDate => _contentNode.CreateDate; - - /// - public override int WriterId => ContentData.WriterId; - - /// - public override DateTime UpdateDate => ContentData.VersionDate; - - // ReSharper disable once CollectionNeverUpdated.Local - private static readonly IReadOnlyDictionary EmptyCultures = - new Dictionary(); - - private IReadOnlyDictionary? _cultures; - - /// - public override IReadOnlyDictionary Cultures - { - get - { - if (_cultures != null) - { - return _cultures; - } - - if (!ContentType.VariesByCulture()) - { - return _cultures = new Dictionary - { - { string.Empty, new PublishedCultureInfo(string.Empty, ContentData.Name, _urlSegment, CreateDate) }, - }; - } - - if (ContentData.CultureInfos == null) - { - throw new PanicException("_contentDate.CultureInfos is null."); - } - - return _cultures = ContentData.CultureInfos - .ToDictionary( - x => x.Key, - x => new PublishedCultureInfo(x.Key, x.Value.Name, x.Value.UrlSegment, x.Value.Date), - StringComparer.OrdinalIgnoreCase); - } - } - - /// - public override PublishedItemType ItemType => _contentNode.ContentType.ItemType; - - /// - public override bool IsDraft(string? culture = null) - { - // if this is the 'published' published content, nothing can be draft - if (ContentData.Published) - { - return false; - } - - // not the 'published' published content, and does not vary = must be draft - if (!ContentType.VariesByCulture()) - { - return true; - } - - // handle context culture - if (culture == null) - { - culture = VariationContextAccessor?.VariationContext?.Culture ?? string.Empty; - } - - // not the 'published' published content, and varies - // = depends on the culture - return ContentData.CultureInfos is not null && - ContentData.CultureInfos.TryGetValue(culture, out CultureVariation? cvar) && cvar.IsDraft; - } - - /// - public override bool IsPublished(string? culture = null) - { - // whether we are the 'draft' or 'published' content, need to determine whether - // there is a 'published' version for the specified culture (or at all, for - // invariant content items) - - // if there is no 'published' published content, no culture can be published - if (!_contentNode.HasPublished) - { - return false; - } - - // if there is a 'published' published content, and does not vary = published - if (!ContentType.VariesByCulture()) - { - return true; - } - - // handle context culture - if (culture == null) - { - culture = VariationContextAccessor?.VariationContext?.Culture ?? string.Empty; - } - - // there is a 'published' published content, and varies - // = depends on the culture - return _contentNode.HasPublishedCulture(culture); - } - - #endregion - - #region Tree - - /// - public override IPublishedContent? Parent - { - get - { - Func getById = GetGetterById(); - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - return getById(publishedSnapshot, IsPreviewing, ParentId); - } - } - - /// - public override IEnumerable ChildrenForAllCultures - { - get - { - Func getById = GetGetterById(); - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - var id = _contentNode.FirstChildContentId; - - while (id > 0) - { - // is IsPreviewing is false, then this can return null - IPublishedContent? content = getById(publishedSnapshot, IsPreviewing, id); - - if (content != null) - { - yield return content; - } - else - { - // but if IsPreviewing is true, we should have a child - if (IsPreviewing) - { - throw new PanicException($"failed to get content with id={id}"); - } - - // if IsPreviewing is false, get the unpublished child nevertheless - // we need it to keep enumerating children! but we don't return it - content = getById(publishedSnapshot, true, id); - if (content == null) - { - throw new PanicException($"failed to get content with id={id}"); - } - } - - var next = UnwrapIPublishedContent(content)._contentNode.NextSiblingContentId; - -#if DEBUG - // I've seen this happen but I think that may have been due to corrupt DB data due to my own - // bugs, but I'm leaving this here just in case we encounter it again while we're debugging. - if (next == id) - { - throw new PanicException($"The current content id {id} is the same as it's next sibling id {next}"); - } -#endif - - id = next; - } - } - } - - #endregion - - #region Properties - - /// - public override IEnumerable Properties => PropertiesArray; - - /// - public override IPublishedProperty? GetProperty(string alias) - { - var index = _contentNode.ContentType.GetPropertyIndex(alias); - if (index < 0) - { - return null; // happens when 'alias' does not match a content type property alias - } - - // should never happen - properties array must be in sync with property type - if (index >= PropertiesArray.Length) - { - throw new IndexOutOfRangeException( - "Index points outside the properties array, which means the properties array is corrupt."); - } - - IPublishedProperty property = PropertiesArray[index]; - return property; - } - - #endregion - - #region Caching - - // beware what you use that one for - you don't want to cache its result - private IAppCache? GetAppropriateCache() - { - IPublishedSnapshot? publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - IAppCache? cache = publishedSnapshot == null - ? null - : (IsPreviewing == false || PublishedSnapshotService.FullCacheWhenPreviewing) && - ContentType.ItemType != PublishedItemType.Member - ? publishedSnapshot.ElementsCache - : publishedSnapshot.SnapshotCache; - return cache; - } - - private IAppCache? GetCurrentSnapshotCache() - { - IPublishedSnapshot? publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - return publishedSnapshot?.SnapshotCache; - } - - #endregion - - #region Internal - - // used by property - internal IVariationContextAccessor VariationContextAccessor { get; } - - // used by navigable content - internal IPublishedProperty[] PropertiesArray { get; } - - // used by navigable content - internal int ParentId => _contentNode.ParentContentId; - - // used by navigable content - // includes all children, published or unpublished - // NavigableNavigator takes care of selecting those it wants - // note: this is not efficient - we do not try to be (would require a double-linked list) - internal IList? ChildIds => Children?.Select(x => x.Id).ToList(); - - // used by Property - // gets a value indicating whether the content or media exists in - // a previewing context or not, ie whether its Parent, Children, and - // properties should refer to published, or draft content - internal bool IsPreviewing { get; } - - private string? _asPreviewingCacheKey; - - private string AsPreviewingCacheKey => - _asPreviewingCacheKey ?? (_asPreviewingCacheKey = CacheKeys.PublishedContentAsPreviewing(Key)); - - // used by ContentCache - internal IPublishedContent? AsDraft() - { - if (IsPreviewing) - { - return this; - } - - IAppCache? cache = GetAppropriateCache(); - if (cache == null) - { - return new PublishedContent(this).CreateModel(_publishedModelFactory); - } - - return (IPublishedContent?)cache.Get(AsPreviewingCacheKey, () => new PublishedContent(this).CreateModel(_publishedModelFactory)); - } - - // used by Navigable.Source,... - internal static PublishedContent UnwrapIPublishedContent(IPublishedContent content) - { - while (content is PublishedContentWrapped wrapped) - { - content = wrapped.Unwrap(); - } - - if (!(content is PublishedContent inner)) - { - throw new InvalidOperationException("Innermost content is not PublishedContent."); - } - - return inner; - } - - #endregion -} diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedMember.cs b/src/Umbraco.PublishedCache.NuCache/PublishedMember.cs deleted file mode 100644 index 32d0ff7039..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/PublishedMember.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -// note -// the whole PublishedMember thing should be refactored because as soon as a member -// is wrapped on in a model, the inner IMember and all associated properties are lost -internal class PublishedMember : PublishedContent, IPublishedMember -{ - private PublishedMember( - IMember member, - ContentNode contentNode, - ContentData contentData, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IPublishedModelFactory publishedModelFactory) - : base(contentNode, contentData, publishedSnapshotAccessor, variationContextAccessor, publishedModelFactory) => - Member = member; - - public static IPublishedContent? Create( - IMember member, - IPublishedContentType contentType, - bool previewing, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IPublishedModelFactory publishedModelFactory) - { - var d = new ContentData(member.Name, null, 0, member.UpdateDate, member.CreatorId, -1, previewing, GetPropertyValues(contentType, member), null); - - var n = new ContentNode( - member.Id, - member.Key, - contentType, - member.Level, - member.Path, - member.SortOrder, - member.ParentId, - member.CreateDate, - member.CreatorId); - - return new PublishedMember(member, n, d, publishedSnapshotAccessor, variationContextAccessor, publishedModelFactory) - .CreateModel(publishedModelFactory); - } - - private static Dictionary GetPropertyValues(IPublishedContentType contentType, IMember member) - { - // see node in PublishedSnapshotService - // we do not (want to) support ConvertDbToXml/String - - // var propertyEditorResolver = PropertyEditorResolver.Current; - - // see note in MemberType.Variations - // we don't want to support variations on members - var properties = member - .Properties - - // .Select(property => - // { - // var e = propertyEditorResolver.GetByAlias(property.PropertyType.PropertyEditorAlias); - // var v = e == null - // ? property.Value - // : e.ValueEditor.ConvertDbToString(property, property.PropertyType, ApplicationContext.Current.Services.DataTypeService); - // return new KeyValuePair(property.Alias, v); - // }) - // .ToDictionary(x => x.Key, x => x.Value); - .ToDictionary( - x => x.Alias, - x => new[] { new PropertyData { Value = x.GetValue(), Culture = string.Empty, Segment = string.Empty } }, - StringComparer.OrdinalIgnoreCase); - - // see also PublishedContentType - AddIf(contentType, properties, nameof(IMember.Email), member.Email); - AddIf(contentType, properties, nameof(IMember.Username), member.Username); - AddIf(contentType, properties, nameof(IMember.Comments), member.Comments); - AddIf(contentType, properties, nameof(IMember.IsApproved), member.IsApproved); - AddIf(contentType, properties, nameof(IMember.IsLockedOut), member.IsLockedOut); - AddIf(contentType, properties, nameof(IMember.LastLockoutDate), member.LastLockoutDate); - AddIf(contentType, properties, nameof(IMember.CreateDate), member.CreateDate); - AddIf(contentType, properties, nameof(IMember.LastLoginDate), member.LastLoginDate); - AddIf(contentType, properties, nameof(IMember.LastPasswordChangeDate), member.LastPasswordChangeDate); - - return properties; - } - - private static void AddIf(IPublishedContentType contentType, IDictionary properties, string alias, object? value) - { - IPublishedPropertyType? propertyType = contentType.GetPropertyType(alias); - if (propertyType == null || propertyType.IsUserProperty) - { - return; - } - - properties[alias] = new[] { new PropertyData { Value = value, Culture = string.Empty, Segment = string.Empty } }; - } - - #region IPublishedMember - - public IMember Member { get; } - - public string Email => Member.Email; - - public string UserName => Member.Username; - - public string? Comments => Member.Comments; - - public bool IsApproved => Member.IsApproved; - - public bool IsLockedOut => Member.IsLockedOut; - - public DateTime? LastLockoutDate => Member.LastLockoutDate; - - public DateTime CreationDate => Member.CreateDate; - - public DateTime? LastLoginDate => Member.LastLoginDate; - - public DateTime? LastPasswordChangedDate => Member.LastPasswordChangeDate; - - #endregion -} diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshot.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshot.cs deleted file mode 100644 index 07bd79ee0c..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshot.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.PublishedCache; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -// implements published snapshot -public class PublishedSnapshot : IPublishedSnapshot -{ - private readonly PublishedSnapshotService? _service; - private bool _defaultPreview; - private PublishedSnapshotElements? _elements; - - #region Constructors - - public PublishedSnapshot(IPublishedSnapshotService service, bool defaultPreview) - { - _service = service as PublishedSnapshotService; - _defaultPreview = defaultPreview; - } - - public class PublishedSnapshotElements : IDisposable - { - public void Dispose() - { - ContentCache?.Dispose(); - MediaCache?.Dispose(); - MemberCache?.Dispose(); - } -#pragma warning disable IDE1006 // Naming Styles - public ContentCache? ContentCache; - public MediaCache? MediaCache; - public MemberCache? MemberCache; - public DomainCache? DomainCache; - public IAppCache? SnapshotCache; - public IAppCache? ElementsCache; -#pragma warning restore IDE1006 // Naming Styles - } - - private PublishedSnapshotElements Elements - { - get - { - if (_service == null) - { - throw new InvalidOperationException( - $"The {typeof(PublishedSnapshot)} cannot be used when the {typeof(IPublishedSnapshotService)} is not the default type {typeof(PublishedSnapshotService)}"); - } - - return _elements ??= _service.GetElements(_defaultPreview); - } - } - - public void Resync() - { - // no lock - published snapshots are single-thread - _elements?.Dispose(); - _elements = null; - } - - #endregion - - #region Caches - - public IAppCache? SnapshotCache => Elements.SnapshotCache; - - public IAppCache? ElementsCache => Elements.ElementsCache; - - #endregion - - #region IPublishedSnapshot - - public IPublishedContentCache? Content => Elements.ContentCache; - - public IPublishedMediaCache? Media => Elements.MediaCache; - - public IPublishedMemberCache? Members => Elements.MemberCache; - - public IDomainCache? Domains => Elements.DomainCache; - - public IDisposable ForcedPreview(bool preview, Action? callback = null) => - new ForcedPreviewObject(this, preview, callback); - - private class ForcedPreviewObject : DisposableObjectSlim - { - private readonly Action? _callback; - private readonly bool _origPreview; - private readonly PublishedSnapshot _publishedSnapshot; - - public ForcedPreviewObject(PublishedSnapshot publishedShapshot, bool preview, Action? callback) - { - _publishedSnapshot = publishedShapshot; - _callback = callback; - - // save and force - _origPreview = publishedShapshot._defaultPreview; - publishedShapshot._defaultPreview = preview; - } - - protected override void DisposeResources() - { - // restore - _publishedSnapshot._defaultPreview = _origPreview; - _callback?.Invoke(_origPreview); - } - } - - #endregion - - #region IDisposable - - private bool _disposed; - - public void Dispose() - { - if (_disposed) - { - return; - } - - _disposed = true; - _elements?.Dispose(); - } - - #endregion -} diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs deleted file mode 100644 index 8aa012d11f..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ /dev/null @@ -1,1245 +0,0 @@ -using CSharpTest.Net.Collections; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Logging; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Runtime; -using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Services.Changes; -using Umbraco.Cms.Core.Sync; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; -using Umbraco.Extensions; -using File = System.IO.File; -using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -internal class PublishedSnapshotService : IPublishedSnapshotService -{ - // define constant - determines whether to use cache when previewing - // to store eg routes, property converted values, anything - caching - // means faster execution, but uses memory - not sure if we want it - // so making it configurable. - public static readonly bool FullCacheWhenPreviewing = true; - private readonly NuCacheSettings _config; - private readonly ContentDataSerializer _contentDataSerializer; - private readonly IDefaultCultureAccessor _defaultCultureAccessor; - private readonly object _elementsLock = new(); - private readonly GlobalSettings _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IMainDom _mainDom; - private readonly PublishedSnapshotServiceOptions _options; - private readonly IProfilingLogger _profilingLogger; - private readonly INuCacheContentService _publishedContentService; - private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; - private readonly IPublishedModelFactory _publishedModelFactory; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly IScopeProvider _scopeProvider; - private readonly ServiceContext _serviceContext; - private readonly object _storesLock = new(); - private readonly ISyncBootStateAccessor _syncBootStateAccessor; - private readonly IVariationContextAccessor _variationContextAccessor; - - private long _contentGen; - - private ContentStore _contentStore = null!; - private long _domainGen; - private SnapDictionary _domainStore = null!; - private IAppCache? _elementsCache; - - private bool _isReadSet; - private bool _isReady; - private object? _isReadyLock; - - private bool _mainDomRegistered; - - private BPlusTree? _localContentDb; - private bool _localContentDbExists; - private BPlusTree? _localMediaDb; - private bool _localMediaDbExists; - private long _mediaGen; - private ContentStore _mediaStore = null!; - - private string LocalFilePath => Path.Combine(_hostingEnvironment.LocalTempPath, "NuCache"); - - public PublishedSnapshotService( - PublishedSnapshotServiceOptions options, - ISyncBootStateAccessor syncBootStateAccessor, - IMainDom mainDom, - ServiceContext serviceContext, - IPublishedContentTypeFactory publishedContentTypeFactory, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IProfilingLogger profilingLogger, - ILoggerFactory loggerFactory, - IScopeProvider scopeProvider, - INuCacheContentService publishedContentService, - IDefaultCultureAccessor defaultCultureAccessor, - IOptions globalSettings, - IPublishedModelFactory publishedModelFactory, - IHostingEnvironment hostingEnvironment, - IOptions config, - ContentDataSerializer contentDataSerializer) - { - _options = options; - _syncBootStateAccessor = syncBootStateAccessor; - _mainDom = mainDom; - _serviceContext = serviceContext; - _publishedContentTypeFactory = publishedContentTypeFactory; - _publishedSnapshotAccessor = publishedSnapshotAccessor; - _variationContextAccessor = variationContextAccessor; - _profilingLogger = profilingLogger; - _loggerFactory = loggerFactory; - _logger = _loggerFactory.CreateLogger(); - _scopeProvider = scopeProvider; - _publishedContentService = publishedContentService; - _defaultCultureAccessor = defaultCultureAccessor; - _globalSettings = globalSettings.Value; - _hostingEnvironment = hostingEnvironment; - _contentDataSerializer = contentDataSerializer; - _config = config.Value; - _publishedModelFactory = publishedModelFactory; - } - - protected PublishedSnapshot? CurrentPublishedSnapshot - { - get - { - _publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot); - return (PublishedSnapshot?)publishedSnapshot; - } - } - - // note: if the service is not ready, ie _isReady is false, then notifications are ignored - - // SetUmbracoVersionStep issues a DistributedCache.Instance.RefreshAll...() call which should cause - // the entire content, media etc caches to reload from database -- and then the app restarts -- however, - // at the time SetUmbracoVersionStep runs, Umbraco is not fully initialized and therefore some property - // value converters, etc are not registered, and rebuilding the NuCache may not work properly. - // - // More details: ApplicationContext.IsConfigured being false, ApplicationEventHandler.ExecuteWhen... is - // called and in most cases events are skipped, so property value converters are not registered or - // removed, so PublishedPropertyType either initializes with the wrong converter, or throws because it - // detects more than one converter for a property type. - // - // It's not an issue for XmlStore - the app restart takes place *after* the install has refreshed the - // cache, and XmlStore just writes a new umbraco.config file upon RefreshAll, so that's OK. - // - // But for NuCache... we cannot rebuild the cache now. So it will NOT work and we are not fixing it, - // because now we should ALWAYS run with the database server messenger, and then the RefreshAll will - // be processed as soon as we are configured and the messenger processes instructions. - - // note: notifications for content type and data type changes should be invoked with the - // InMemoryModelFactory, if any, locked and refreshed - see ContentTypeCacheRefresher and - // DataTypeCacheRefresher - public void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged) - { - EnsureCaches(); - - using (_contentStore.GetScopedWriteLock(_scopeProvider)) - { - NotifyLocked(payloads, out var draftChanged2, out var publishedChanged2); - draftChanged = draftChanged2; - publishedChanged = publishedChanged2; - } - - if (draftChanged || publishedChanged) - { - CurrentPublishedSnapshot?.Resync(); - } - } - - /// - public void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged) - { - EnsureCaches(); - - using (_mediaStore.GetScopedWriteLock(_scopeProvider)) - { - NotifyLocked(payloads, out var anythingChanged2); - anythingChanged = anythingChanged2; - } - - if (anythingChanged) - { - CurrentPublishedSnapshot?.Resync(); - } - } - - /// - public void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads) - { - EnsureCaches(); - - foreach (ContentTypeCacheRefresher.JsonPayload payload in payloads) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("Notified {ChangeTypes} for {ItemType} {ItemId}", payload.ChangeTypes, payload.ItemType, payload.Id); - } - } - - // Ensure all published data types are updated - _publishedContentTypeFactory.NotifyDataTypeChanges(); - - Notify(_contentStore, payloads, RefreshContentTypesLocked); - Notify(_mediaStore, payloads, RefreshMediaTypesLocked); - - if (_publishedModelFactory.IsLiveFactoryEnabled()) - { - // In the case of ModelsMode.InMemoryAuto generated models - we actually need to refresh all of the content and the media - // see https://github.com/umbraco/Umbraco-CMS/issues/5671 - // The underlying issue is that in ModelsMode.InMemoryAuto mode the IAutoPublishedModelFactory will re-compile all of the classes/models - // into a new DLL for the application which includes both content types and media types. - // Since the models in the cache are based on these actual classes, all of the objects in the cache need to be updated - // to use the newest version of the class. - - // NOTE: Ideally this can be run on background threads here which would prevent blocking the UI - // as is the case when saving a content type. Initially one would think that it won't be any different - // between running this here or in another background thread immediately after with regards to how the - // UI will respond because we already know between calling `WithSafeLiveFactoryReset` to reset the generated models - // and this code here, that many front-end requests could be attempted to be processed. If that is the case, those pages are going to get a - // model binding error and our ModelBindingExceptionFilter is going to to its magic to reload those pages so the end user is none the wiser. - // So whether or not this executes 'here' or on a background thread immediately wouldn't seem to make any difference except that we can return - // execution to the UI sooner. - // BUT!... there is a difference IIRC. There is still execution logic that continues after this call on this thread with the cache refreshers - // and those cache refreshers need to have the up-to-date data since other user cache refreshers will be expecting the data to be 'live'. If - // we ran this on a background thread then those cache refreshers are going to not get 'live' data when they query the content cache which - // they require. - using (_contentStore.GetScopedWriteLock(_scopeProvider)) - { - NotifyLocked( - new[] - { - new ContentCacheRefresher.JsonPayload() - { - ChangeTypes = TreeChangeTypes.RefreshAll - } - }, - out _, - out _); - } - - using (_mediaStore.GetScopedWriteLock(_scopeProvider)) - { - NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _); - } - } - - CurrentPublishedSnapshot?.Resync(); - } - - public void Notify(DataTypeCacheRefresher.JsonPayload[] payloads) - { - EnsureCaches(); - - var idsA = payloads.Select(x => x.Id).ToArray(); - - foreach (DataTypeCacheRefresher.JsonPayload payload in payloads) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug( - "Notified {RemovedStatus} for data type {DataTypeId}", - payload.Removed ? "Removed" : "Refreshed", - payload.Id); - } - } - - using (_contentStore.GetScopedWriteLock(_scopeProvider)) - using (_mediaStore.GetScopedWriteLock(_scopeProvider)) - { - // TODO: need to add a datatype lock - // this is triggering datatypes reload in the factory, and right after we create some - // content types by loading them ... there's a race condition here, which would require - // some locking on datatypes - _publishedContentTypeFactory.NotifyDataTypeChanges(idsA); - - using (IScope scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.ContentTree); - _contentStore.UpdateDataTypesLocked(idsA, id => CreateContentType(PublishedItemType.Content, id)); - scope.Complete(); - } - - using (IScope scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.MediaTree); - _mediaStore.UpdateDataTypesLocked(idsA, id => CreateContentType(PublishedItemType.Media, id)); - scope.Complete(); - } - } - - CurrentPublishedSnapshot?.Resync(); - } - - public void Notify(DomainCacheRefresher.JsonPayload[] payloads) - { - EnsureCaches(); - - // see note in LockAndLoadContent - using (_domainStore.GetScopedWriteLock(_scopeProvider)) - { - foreach (DomainCacheRefresher.JsonPayload payload in payloads) - { - switch (payload.ChangeType) - { - case DomainChangeTypes.RefreshAll: - using (IScope scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.Domains); - LoadDomainsLocked(); - scope.Complete(); - } - - break; - case DomainChangeTypes.Remove: - _domainStore.ClearLocked(payload.Id); - break; - case DomainChangeTypes.Refresh: - IDomain? domain = _serviceContext.DomainService?.GetById(payload.Id); - if (domain == null) - { - continue; - } - - if (domain.RootContentId.HasValue == false) - { - continue; // anomaly - } - - var culture = domain.LanguageIsoCode; - if (string.IsNullOrWhiteSpace(culture)) - { - continue; // anomaly - } - - _domainStore.SetLocked( - domain.Id, - new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard, domain.SortOrder)); - break; - } - } - } - } - - public IPublishedSnapshot CreatePublishedSnapshot(string? previewToken) - { - EnsureCaches(); - - // no cache, no joy - if (Volatile.Read(ref _isReady) == false) - { - throw new InvalidOperationException("The published snapshot service has not properly initialized."); - } - - var preview = previewToken.IsNullOrWhiteSpace() == false; - return new PublishedSnapshot(this, preview); - } - - /// - public void Rebuild( - IReadOnlyCollection? contentTypeIds = null, - IReadOnlyCollection? mediaTypeIds = null, - IReadOnlyCollection? memberTypeIds = null) - => _publishedContentService.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds); - - public async Task CollectAsync() - { - EnsureCaches(); - - await _contentStore.CollectAsync(); - await _mediaStore.CollectAsync(); - } - - /// - public void Dispose() - { - } - - // gets a new set of elements - // always creates a new set of elements, - // even though the underlying elements may not change (store snapshots) - public PublishedSnapshot.PublishedSnapshotElements GetElements(bool previewDefault) - { - EnsureCaches(); - - // note: using ObjectCacheAppCache for elements and snapshot caches - // is not recommended because it creates an inner MemoryCache which is a heavy - // thing - better use a dictionary-based cache which "just" creates a concurrent - // dictionary - - // for snapshot cache, DictionaryAppCache MAY be OK but it is not thread-safe, - // nothing like that... - // for elements cache, DictionaryAppCache is a No-No, use something better. - // ie FastDictionaryAppCache (thread safe and all) - ContentStore.Snapshot contentSnap, mediaSnap; - SnapDictionary.Snapshot domainSnap; - IAppCache? elementsCache; - - // Here we are reading/writing to shared objects so we need to lock (can't be _storesLock which manages the actual nucache files - // and would result in a deadlock). Even though we are locking around underlying readlocks (within CreateSnapshot) it's because - // we need to ensure that the result of contentSnap.Gen (etc) and the re-assignment of these values and _elements cache - // are done atomically. - lock (_elementsLock) - { - IScopeContext? scopeContext = _scopeProvider.Context; - - if (scopeContext == null) - { - contentSnap = _contentStore.CreateSnapshot(); - mediaSnap = _mediaStore.CreateSnapshot(); - domainSnap = _domainStore.CreateSnapshot(); - elementsCache = _elementsCache; - } - else - { - contentSnap = _contentStore.LiveSnapshot; - mediaSnap = _mediaStore.LiveSnapshot; - domainSnap = _domainStore.Test.LiveSnapshot; - elementsCache = _elementsCache; - - // this is tricky - // we are returning elements composed from live snapshots, which we need to replace - // with actual snapshots when the context is gone - but when the action runs, there - // still is a context - so we cannot get elements - just resync = nulls the current - // elements - // just need to make sure nothing gets elements in another enlisted action... so using - // a MaxValue to make sure this one runs last, and it should be ok - scopeContext.Enlist( - "Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.Resync", - () => this, - (completed, svc) => - { - svc?.CurrentPublishedSnapshot?.Resync(); - }, - int.MaxValue); - } - - // create a new snapshot cache if snapshots are different gens - if (contentSnap.Gen != _contentGen || mediaSnap.Gen != _mediaGen || domainSnap.Gen != _domainGen || - _elementsCache == null) - { - _contentGen = contentSnap.Gen; - _mediaGen = mediaSnap.Gen; - _domainGen = domainSnap.Gen; - elementsCache = _elementsCache = new FastDictionaryAppCache(); - } - } - - var snapshotCache = new DictionaryAppCache(); - - var memberTypeCache = new PublishedContentTypeCache( - null, - null, - _serviceContext.MemberTypeService, - _publishedContentTypeFactory, - _loggerFactory.CreateLogger()); - - var defaultCulture = _defaultCultureAccessor.DefaultCulture; - var domainCache = new DomainCache(domainSnap, defaultCulture); - - return new PublishedSnapshot.PublishedSnapshotElements - { - ContentCache = - new ContentCache(previewDefault, contentSnap, snapshotCache, elementsCache, domainCache, Options.Create(_globalSettings), _variationContextAccessor), - MediaCache = new MediaCache(previewDefault, mediaSnap, _variationContextAccessor), - MemberCache = - new MemberCache(previewDefault, memberTypeCache, _publishedSnapshotAccessor, _variationContextAccessor, _publishedModelFactory), - DomainCache = domainCache, - SnapshotCache = snapshotCache, - ElementsCache = elementsCache, - }; - } - - // NOTE: These aren't used within this object but are made available internally to improve the IdKey lookup performance - // when nucache is enabled. - // TODO: Does this need to be here? - internal int GetDocumentId(Guid udi) - { - EnsureCaches(); - return GetId(_contentStore, udi); - } - - internal int GetMediaId(Guid udi) - { - EnsureCaches(); - return GetId(_mediaStore, udi); - } - - internal Guid GetDocumentUid(int id) - { - EnsureCaches(); - return GetUid(_contentStore, id); - } - - internal Guid GetMediaUid(int id) - { - EnsureCaches(); - return GetUid(_mediaStore, id); - } - - - public void ResetLocalDb() - { - _logger.LogInformation( - "Resetting NuCache local db"); - var path = LocalFilePath; - if (Directory.Exists(path) is false) - { - return; - } - - MainDomRelease(); - Directory.Delete(path, true); - MainDomRegister(); - } - - /// - /// Lazily populates the stores only when they are first requested - /// - internal void EnsureCaches() => LazyInitializer.EnsureInitialized( - ref _isReady, - ref _isReadSet, - ref _isReadyLock, - () => - { - // lock this entire call, we only want a single thread to be accessing the stores at once and within - // the call below to mainDom.Register, a callback may occur on a threadpool thread to MainDomRelease - // at the same time as we are trying to write to the stores. MainDomRelease also locks on _storesLock so - // it will not be able to close the stores until we are done populating (if the store is empty) - lock (_storesLock) - { - SyncBootState bootState = _syncBootStateAccessor.GetSyncBootState(); - - if (!_options.IgnoreLocalDb) - { - if (!_mainDomRegistered) - { - _mainDom.Register(MainDomRegister, MainDomRelease); - } - else - { - // MainDom is already registered, so we must be retrying to load cache data - // We can't trust the localdb state, so always perform a cold boot - bootState = SyncBootState.ColdBoot; - } - - // stores are created with a db so they can write to it, but they do not read from it, - // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to - // figure out whether it can read the databases or it should populate them from sql - _logger.LogInformation( - "Creating the content store, localContentDbExists? {LocalContentDbExists}", - _localContentDbExists); - _contentStore = new ContentStore( - _publishedSnapshotAccessor, - _variationContextAccessor, - _loggerFactory.CreateLogger("ContentStore"), - _loggerFactory, - _publishedModelFactory, - _localContentDb); - _logger.LogInformation( - "Creating the media store, localMediaDbExists? {LocalMediaDbExists}", - _localMediaDbExists); - _mediaStore = new ContentStore( - _publishedSnapshotAccessor, - _variationContextAccessor, - _loggerFactory.CreateLogger("ContentStore"), - _loggerFactory, - _publishedModelFactory, - _localMediaDb); - } - else - { - _logger.LogInformation("Creating the content store (local db ignored)"); - _contentStore = new ContentStore( - _publishedSnapshotAccessor, - _variationContextAccessor, - _loggerFactory.CreateLogger("ContentStore"), - _loggerFactory, - _publishedModelFactory); - _logger.LogInformation("Creating the media store (local db ignored)"); - _mediaStore = new ContentStore( - _publishedSnapshotAccessor, - _variationContextAccessor, - _loggerFactory.CreateLogger("ContentStore"), - _loggerFactory, - _publishedModelFactory); - } - - _domainStore = new SnapDictionary(); - - var okContent = false; - var okMedia = false; - - try - { - if (bootState != SyncBootState.ColdBoot && _localContentDbExists) - { - okContent = LockAndLoadContent(() => LoadContentFromLocalDbLocked(true)); - if (!okContent) - { - _logger.LogWarning( - "Loading content from local db raised warnings, will reload from database."); - } - } - - if (bootState != SyncBootState.ColdBoot && _localMediaDbExists) - { - okMedia = LockAndLoadMedia(() => LoadMediaFromLocalDbLocked(true)); - if (!okMedia) - { - _logger.LogWarning( - "Loading media from local db raised warnings, will reload from database."); - } - } - - if (!okContent) - { - LockAndLoadContent(() => LoadContentFromDatabaseLocked(true)); - } - - if (!okMedia) - { - LockAndLoadMedia(() => LoadMediaFromDatabaseLocked(true)); - } - - LockAndLoadDomains(); - } - catch (Exception ex) - { - _logger.LogCritical(ex, "Panic, exception while loading cache data."); - throw; - } - - return true; - } - }); - - private int GetId(ContentStore? store, Guid uid) => store?.LiveSnapshot.Get(uid)?.Id ?? 0; - - private Guid GetUid(ContentStore? store, int id) => store?.LiveSnapshot.Get(id)?.Uid ?? Guid.Empty; - - /// - /// Install phase of - /// - /// - /// This is inside of a lock in MainDom so this is guaranteed to run if MainDom was acquired and guaranteed - /// to not run if MainDom wasn't acquired. - /// If MainDom was not acquired, then _localContentDb and _localMediaDb will remain null which means this appdomain - /// will load in published content via the DB and in that case this appdomain will probably not exist long enough to - /// serve more than a page of content. - /// - private void MainDomRegister() - { - var path = GetAndEnsureLocalFilesPathExists(); - var localContentDbPath = Path.Combine(path, "NuCache.Content.db"); - var localMediaDbPath = Path.Combine(path, "NuCache.Media.db"); - - _localContentDbExists = File.Exists(localContentDbPath); - _localMediaDbExists = File.Exists(localMediaDbPath); - - // if both local databases exist then GetTree will open them, else new databases will be created - _localContentDb = BTree.GetTree(localContentDbPath, _localContentDbExists, _config, _contentDataSerializer); - _localMediaDb = BTree.GetTree(localMediaDbPath, _localMediaDbExists, _config, _contentDataSerializer); - - _mainDomRegistered = true; - - _logger.LogInformation( - "Registered with MainDom, localContentDbExists? {LocalContentDbExists}, localMediaDbExists? {LocalMediaDbExists}", - _localContentDbExists, - _localMediaDbExists); - } - - /// - /// Release phase of MainDom - /// - /// - /// This will execute on a threadpool thread - /// - private void MainDomRelease() - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("Releasing from MainDom..."); - } - - lock (_storesLock) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("Releasing content store..."); - } - _contentStore?.ReleaseLocalDb(); // null check because we could shut down before being assigned - _localContentDb = null; - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("Releasing media store..."); - } - _mediaStore?.ReleaseLocalDb(); // null check because we could shut down before being assigned - _localMediaDb = null; - - _logger.LogInformation("Released from MainDom"); - } - } - - private string GetAndEnsureLocalFilesPathExists() - { - var path = LocalFilePath; - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - } - - return path; - } - - // sudden panic... but in RepeatableRead can a content that I haven't already read, be removed - // before I read it? NO! because the WHOLE content tree is read-locked using WithReadLocked. - // don't panic. - private bool LockAndLoadContent(Func action) - { - // first get a writer, then a scope - // if there already is a scope, the writer will attach to it - // otherwise, it will only exist here - cheap - using (_contentStore?.GetScopedWriteLock(_scopeProvider)) - using (IScope scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.ContentTree); - var ok = action(); - scope.Complete(); - return ok; - } - } - - private bool LoadContentFromDatabaseLocked(bool onStartup) - { - // locks: - // contentStore is wlocked (1 thread) - // content (and types) are read-locked - var contentTypes = _serviceContext.ContentTypeService?.GetAll().ToList(); - - _contentStore.SetAllContentTypesLocked(contentTypes?.Select(x => - _publishedContentTypeFactory.CreateContentType(x))); - - using (_profilingLogger.TraceDuration("Loading content from database")) - { - // beware! at that point the cache is inconsistent, - // assuming we are going to SetAll content items! - _localContentDb?.Clear(); - - // IMPORTANT GetAllContentSources sorts kits by level + parentId + sortOrder - IEnumerable kits = _publishedContentService.GetAllContentSources(); - return onStartup - ? _contentStore.SetAllFastSortedLocked(kits, _config.KitBatchSize, true) - : _contentStore.SetAllLocked(kits, _config.KitBatchSize, true); - } - } - - private bool LoadContentFromLocalDbLocked(bool onStartup) - { - IEnumerable? contentTypes = _serviceContext.ContentTypeService?.GetAll() - .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _contentStore.SetAllContentTypesLocked(contentTypes); - - using (_profilingLogger.TraceDuration("Loading content from local cache file")) - { - // beware! at that point the cache is inconsistent, - // assuming we are going to SetAll content items! - return LoadEntitiesFromLocalDbLocked(onStartup, _localContentDb, _contentStore, "content"); - } - } - - private bool LockAndLoadMedia(Func action) - { - // see note in LockAndLoadContent - using (_mediaStore?.GetScopedWriteLock(_scopeProvider)) - using (IScope scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.MediaTree); - var ok = action(); - scope.Complete(); - return ok; - } - } - - private bool LoadMediaFromDatabaseLocked(bool onStartup) - { - // locks & notes: see content - IEnumerable? mediaTypes = _serviceContext.MediaTypeService?.GetAll() - .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _mediaStore.SetAllContentTypesLocked(mediaTypes); - - using (_profilingLogger.TraceDuration("Loading media from database")) - { - // beware! at that point the cache is inconsistent, - // assuming we are going to SetAll content items! - _localMediaDb?.Clear(); - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("Loading media from database..."); - } - - // IMPORTANT GetAllMediaSources sorts kits by level + parentId + sortOrder - IEnumerable kits = _publishedContentService.GetAllMediaSources(); - return onStartup - ? _mediaStore.SetAllFastSortedLocked(kits, _config.KitBatchSize, true) - : _mediaStore.SetAllLocked(kits, _config.KitBatchSize, true); - } - } - - private bool LoadMediaFromLocalDbLocked(bool onStartup) - { - IEnumerable? mediaTypes = _serviceContext.MediaTypeService?.GetAll() - .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _mediaStore.SetAllContentTypesLocked(mediaTypes); - - using (_profilingLogger.TraceDuration("Loading media from local cache file")) - { - // beware! at that point the cache is inconsistent, - // assuming we are going to SetAll content items! - return LoadEntitiesFromLocalDbLocked(onStartup, _localMediaDb, _mediaStore, "media"); - } - } - - private bool LoadEntitiesFromLocalDbLocked(bool onStartup, BPlusTree? localDb, ContentStore store, string entityType) - { - var kits = localDb?.Select(x => x.Value) - .OrderBy(x => x.Node.Level) - .ThenBy(x => x.Node.ParentContentId) - .ThenBy(x => x.Node.SortOrder) // IMPORTANT sort by level + parentId + sortOrder - .ToList(); - - if (kits is null || kits.Count == 0) - { - // If there's nothing in the local cache file, we should return false? YES even though the site legitately might be empty. - // Is it possible that the cache file is empty but the database is not? YES... (well, it used to be possible) - // * A new file is created when one doesn't exist, this will only be done when MainDom is acquired - // * The new file will be populated as soon as LoadCachesOnStartup is called - // * If the appdomain is going down the moment after MainDom was acquired and we've created an empty cache file, - // then the MainDom release callback is triggered from on a different thread, which will close the file and - // set the cache file reference to null. At this moment, it is possible that the file is closed and the - // reference is set to null BEFORE LoadCachesOnStartup which would mean that the current appdomain would load - // in the in-mem cache via DB calls, BUT this now means that there is an empty cache file which will be - // loaded by the next appdomain and it won't check if it's empty, it just assumes that since the cache - // file is there, that is correct. - - // Update: We will still return false here even though the above mentioned race condition has been fixed since we now - // lock the entire operation of creating/populating the cache file with the same lock as releasing/closing the cache file - _logger.LogInformation( - "Tried to load {entityType} from the local cache file but it was empty.", - entityType); - return false; - } - - return onStartup - ? store.SetAllFastSortedLocked(kits, _config.KitBatchSize, false) - : store.SetAllLocked(kits, _config.KitBatchSize, false); - } - - private void LockAndLoadDomains() - { - // see note in LockAndLoadContent - using (_domainStore?.GetScopedWriteLock(_scopeProvider)) - using (IScope scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.Domains); - LoadDomainsLocked(); - scope.Complete(); - } - } - - private void LoadDomainsLocked() - { - IEnumerable? domains = _serviceContext.DomainService?.GetAll(true); - if (domains is not null) - { - foreach (Domain domain in domains - .Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false) - .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId!.Value, x.LanguageIsoCode!, x.IsWildcard, x.SortOrder))) - { - _domainStore.SetLocked(domain.Id, domain); - } - } - } - - // Calling this method means we have a lock on the contentStore (i.e. GetScopedWriteLock) - private void NotifyLocked(IEnumerable payloads, out bool draftChanged, out bool publishedChanged) - { - publishedChanged = false; - draftChanged = false; - - // locks: - // content (and content types) are read-locked while reading content - // contentStore is wlocked (so readable, only no new views) - // and it can be wlocked by 1 thread only at a time - // contentStore is write-locked during changes - see note above, calls to this method are wrapped in contentStore.GetScopedWriteLock - foreach (ContentCacheRefresher.JsonPayload payload in payloads) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("Notified {ChangeTypes} for content {ContentId}", payload.ChangeTypes, payload.Id); - } - - if (payload.Blueprint) - { - // Skip blueprints - continue; - } - - if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) - { - using (IScope scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.ContentTree); - LoadContentFromDatabaseLocked(false); - scope.Complete(); - } - - draftChanged = publishedChanged = true; - continue; - } - - if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) - { - if (_contentStore.ClearLocked(payload.Id)) - { - draftChanged = publishedChanged = true; - } - - continue; - } - - if (payload.ChangeTypes.HasTypesNone(TreeChangeTypes.RefreshNode | TreeChangeTypes.RefreshBranch)) - { - // ?! - continue; - } - - // TODO: should we do some RV check here? (later) - ContentCacheRefresher.JsonPayload capture = payload; - using (IScope scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.ContentTree); - - if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) - { - // ?? should we do some RV check here? - // IMPORTANT GetbranchContentSources sorts kits by level and by sort order - IEnumerable kits = _publishedContentService.GetBranchContentSources(capture.Id); - _contentStore.SetBranchLocked(capture.Id, kits); - } - else - { - // ?? should we do some RV check here? - ContentNodeKit kit = _publishedContentService.GetContentSource(capture.Id); - if (kit.IsEmpty) - { - _contentStore.ClearLocked(capture.Id); - } - else - { - _contentStore.SetLocked(kit); - } - } - - scope.Complete(); - } - - // ?? cannot tell really because we're not doing RV checks - draftChanged = publishedChanged = true; - } - } - - private void NotifyLocked(IEnumerable payloads, out bool anythingChanged) - { - anythingChanged = false; - - // locks: - // see notes for content cache refresher - foreach (MediaCacheRefresher.JsonPayload payload in payloads) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("Notified {ChangeTypes} for media {MediaId}", payload.ChangeTypes, payload.Id); - } - - if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) - { - using (IScope scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.MediaTree); - LoadMediaFromDatabaseLocked(false); - scope.Complete(); - } - - anythingChanged = true; - continue; - } - - if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) - { - if (_mediaStore.ClearLocked(payload.Id)) - { - anythingChanged = true; - } - - continue; - } - - if (payload.ChangeTypes.HasTypesNone(TreeChangeTypes.RefreshNode | TreeChangeTypes.RefreshBranch)) - { - // ?! - continue; - } - - // TODO: should we do some RV checks here? (later) - MediaCacheRefresher.JsonPayload capture = payload; - using (IScope scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.MediaTree); - - if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) - { - // ?? should we do some RV check here? - // IMPORTANT GetbranchContentSources sorts kits by level and by sort order - IEnumerable kits = _publishedContentService.GetBranchMediaSources(capture.Id); - _mediaStore.SetBranchLocked(capture.Id, kits); - } - else - { - // ?? should we do some RV check here? - ContentNodeKit kit = _publishedContentService.GetMediaSource(capture.Id); - if (kit.IsEmpty) - { - _mediaStore.ClearLocked(capture.Id); - } - else - { - _mediaStore.SetLocked(kit); - } - } - - scope.Complete(); - } - - // ?? cannot tell really because we're not doing RV checks - anythingChanged = true; - } - } - - private void Notify(ContentStore store, ContentTypeCacheRefresher.JsonPayload[] payloads, Action?, List?, List?, List?> action) - where T : IContentTypeComposition - { - if (payloads.Length == 0) - { - return; // nothing to do - } - - var nameOfT = typeof(T).Name; - - List? removedIds = null, refreshedIds = null, otherIds = null, newIds = null; - - foreach (ContentTypeCacheRefresher.JsonPayload payload in payloads) - { - if (payload.ItemType != nameOfT) - { - continue; - } - - if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Remove)) - { - AddToList(ref removedIds, payload.Id); - } - else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshMain)) - { - AddToList(ref refreshedIds, payload.Id); - } - else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshOther)) - { - AddToList(ref otherIds, payload.Id); - } - else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Create)) - { - AddToList(ref newIds, payload.Id); - } - } - - if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && - newIds.IsCollectionEmpty()) - { - return; - } - - using (store.GetScopedWriteLock(_scopeProvider)) - { - action(removedIds, refreshedIds, otherIds, newIds); - } - } - - // Methods used to prevent allocations of lists - private void AddToList(ref List? list, int val) => GetOrCreateList(ref list).Add(val); - - private List GetOrCreateList(ref List? list) => list ??= new List(); - - private IReadOnlyCollection CreateContentTypes(PublishedItemType itemType, int[]? ids) - { - // XxxTypeService.GetAll(empty) returns everything! - if (ids is null || ids.Length == 0) - { - return Array.Empty(); - } - - IEnumerable? contentTypes; - switch (itemType) - { - case PublishedItemType.Content: - contentTypes = _serviceContext.ContentTypeService?.GetAll(ids); - break; - case PublishedItemType.Media: - contentTypes = _serviceContext.MediaTypeService?.GetAll(ids); - break; - case PublishedItemType.Member: - contentTypes = _serviceContext.MemberTypeService?.GetAll(ids); - break; - default: - throw new ArgumentOutOfRangeException(nameof(itemType)); - } - - if (contentTypes is null) - { - return Array.Empty(); - } - - // some may be missing - not checking here - return contentTypes.Select(x => _publishedContentTypeFactory.CreateContentType(x)).ToList(); - } - - private IPublishedContentType? CreateContentType(PublishedItemType itemType, int id) - { - IContentTypeComposition? contentType; - switch (itemType) - { - case PublishedItemType.Content: - contentType = _serviceContext.ContentTypeService?.Get(id); - break; - case PublishedItemType.Media: - contentType = _serviceContext.MediaTypeService?.Get(id); - break; - case PublishedItemType.Member: - contentType = _serviceContext.MemberTypeService?.Get(id); - break; - default: - throw new ArgumentOutOfRangeException(nameof(itemType)); - } - - return contentType == null ? null : _publishedContentTypeFactory.CreateContentType(contentType); - } - - private void RefreshContentTypesLocked(List? removedIds, List? refreshedIds, List? otherIds, List? newIds) - { - if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && - newIds.IsCollectionEmpty()) - { - return; - } - - // locks: - // content (and content types) are read-locked while reading content - // contentStore is wlocked (so readable, only no new views) - // and it can be wlocked by 1 thread only at a time - using (IScope scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.ContentTypes); - - IPublishedContentType[] typesA = refreshedIds.IsCollectionEmpty() - ? Array.Empty() - : CreateContentTypes(PublishedItemType.Content, refreshedIds?.ToArray()).ToArray(); - - ContentNodeKit[] kits = refreshedIds.IsCollectionEmpty() - ? Array.Empty() - : _publishedContentService.GetTypeContentSources(refreshedIds).ToArray(); - - _contentStore.UpdateContentTypesLocked(removedIds, typesA, kits); - if (!otherIds.IsCollectionEmpty()) - { - _contentStore.UpdateContentTypesLocked(CreateContentTypes( - PublishedItemType.Content, - otherIds?.ToArray())); - } - - if (!newIds.IsCollectionEmpty()) - { - _contentStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Content, newIds?.ToArray())); - } - - scope.Complete(); - } - } - - private void RefreshMediaTypesLocked(List? removedIds, List? refreshedIds, List? otherIds, List? newIds) - { - if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && - newIds.IsCollectionEmpty()) - { - return; - } - - // locks: - // media (and content types) are read-locked while reading media - // mediaStore is wlocked (so readable, only no new views) - // and it can be wlocked by 1 thread only at a time - using (IScope scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.MediaTypes); - - IPublishedContentType[] typesA = refreshedIds == null - ? Array.Empty() - : CreateContentTypes(PublishedItemType.Media, refreshedIds.ToArray()).ToArray(); - - ContentNodeKit[] kits = refreshedIds == null - ? Array.Empty() - : _publishedContentService.GetTypeMediaSources(refreshedIds).ToArray(); - - _mediaStore.UpdateContentTypesLocked(removedIds, typesA, kits); - if (!otherIds.IsCollectionEmpty()) - { - _mediaStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Media, otherIds?.ToArray()) - .ToArray()); - } - - if (!newIds.IsCollectionEmpty()) - { - _mediaStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Media, newIds?.ToArray()) - .ToArray()); - } - - scope.Complete(); - } - } - - internal ContentStore GetContentStore() - { - EnsureCaches(); - return _contentStore; - } - - internal ContentStore? GetMediaStore() - { - EnsureCaches(); - return _mediaStore; - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs deleted file mode 100644 index f122077498..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Services.Changes; -using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -/// -/// Subscribes to Umbraco events to ensure nucache remains consistent with the source data -/// -public class PublishedSnapshotServiceEventHandler : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler -{ - private readonly INuCacheContentService _publishedContentService; - private readonly IPublishedSnapshotService _publishedSnapshotService; - - /// - /// Initializes a new instance of the class. - /// - public PublishedSnapshotServiceEventHandler( - IPublishedSnapshotService publishedSnapshotService, - INuCacheContentService publishedContentService) - { - _publishedSnapshotService = publishedSnapshotService; - _publishedContentService = publishedContentService; - } - - [Obsolete("Please use alternative constructor.")] - public PublishedSnapshotServiceEventHandler( - IRuntimeState runtime, - IPublishedSnapshotService publishedSnapshotService, - INuCacheContentService publishedContentService, - IContentService contentService, - IMediaService mediaService) - : this(publishedSnapshotService, publishedContentService) - { - } - - public void Handle(ContentRefreshNotification notification) => - _publishedContentService.RefreshContent(notification.Entity); - - public void Handle(ContentTypeRefreshedNotification notification) - { - const ContentTypeChangeTypes types // only for those that have been refreshed - = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; - var contentTypeIds = notification.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id) - .ToArray(); - if (contentTypeIds.Any()) - { - _publishedSnapshotService.Rebuild(contentTypeIds); - } - } - - // TODO: This should be a cache refresher call! - - /// - /// If a is ever saved with a different culture, we need to rebuild all of the content nucache - /// database table - /// - public void Handle(LanguageSavedNotification notification) - { - // culture changed on an existing language - var cultureChanged = notification.SavedEntities.Any(x => - !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); - if (cultureChanged) - { - // Rebuild all content for all content types - _publishedSnapshotService.Rebuild(Array.Empty()); - } - } - - public void Handle(MediaRefreshNotification notification) => - _publishedContentService.RefreshMedia(notification.Entity); - - public void Handle(MediaTypeRefreshedNotification notification) - { - const ContentTypeChangeTypes types // only for those that have been refreshed - = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; - var mediaTypeIds = notification.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id) - .ToArray(); - if (mediaTypeIds.Any()) - { - _publishedSnapshotService.Rebuild(mediaTypeIds: mediaTypeIds); - } - } - - public void Handle(MemberDeletingNotification notification) => - _publishedContentService.DeleteContentItems(notification.DeletedEntities); - - public void Handle(MemberRefreshNotification notification) => - _publishedContentService.RefreshMember(notification.Entity); - - public void Handle(MemberTypeRefreshedNotification notification) - { - const ContentTypeChangeTypes types // only for those that have been refreshed - = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; - var memberTypeIds = notification.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id) - .ToArray(); - if (memberTypeIds.Any()) - { - _publishedSnapshotService.Rebuild(memberTypeIds: memberTypeIds); - } - } - - // note: if the service is not ready, ie _isReady is false, then we still handle repository events, - // because we can, we do not need a working published snapshot to do it - the only reason why it could cause an - // issue is if the database table is not ready, but that should be prevented by migrations. - - // we need them to be "repository" events ie to trigger from within the repository transaction, - // because they need to be consistent with the content that is being refreshed/removed - and that - // should be guaranteed by a DB transaction - public void Handle(ScopedEntityRemoveNotification notification) => - _publishedContentService.DeleteContentItem(notification.Entity); -} diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceOptions.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceOptions.cs deleted file mode 100644 index 5f73f691e9..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceOptions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Umbraco.Cms.Core.PublishedCache; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -/// -/// Options class for configuring the -/// -public class PublishedSnapshotServiceOptions -{ - // disabled: prevents the published snapshot from updating and exposing changes - // or even creating a new published snapshot to see changes, uses old cache = bad - // - //// indicates that the snapshot cache should reuse the application request cache - //// otherwise a new cache object would be created for the snapshot specifically, - //// which is the default - web boot manager uses this to optimize facades - // public bool PublishedSnapshotCacheIsApplicationRequestCache; - - /// - /// If true this disables the persisted local cache files for content and media - /// - /// - /// By default this is false which means umbraco will use locally persisted cache files for reading in all published - /// content and media on application startup. - /// The reason for this is to improve startup times because the alternative to populating the published content and - /// media on application startup is to read - /// these values from the database. In scenarios where sites are relatively small (below a few thousand nodes) reading - /// the content/media from the database to populate - /// the in memory cache isn't that slow and is only marginally slower than reading from the locally persisted cache - /// files. - /// - public bool IgnoreLocalDb { get; set; } -} diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotStatus.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotStatus.cs deleted file mode 100644 index ea24c5bc1c..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotStatus.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -/// -/// Generates a status report for -/// -internal class PublishedSnapshotStatus : IPublishedSnapshotStatus -{ - private readonly INuCacheContentService _publishedContentService; - private readonly PublishedSnapshotService? _service; - - public PublishedSnapshotStatus(IPublishedSnapshotService? service, INuCacheContentService publishedContentService) - { - _service = service as PublishedSnapshotService; - _publishedContentService = publishedContentService; - } - - /// - public string GetStatus() - { - if (_service == null) - { - return - $"The current {typeof(IPublishedSnapshotService)} is not the default type. A status cannot be determined."; - } - - // TODO: This should be private - _service.EnsureCaches(); - - var dbCacheIsOk = _publishedContentService.VerifyContentDbCache() - && _publishedContentService.VerifyMediaDbCache() - && _publishedContentService.VerifyMemberDbCache() - ? "ok" - : "NOT ok (rebuild?)"; - - ContentStore? contentStore = _service.GetContentStore(); - ContentStore? mediaStore = _service.GetMediaStore(); - - var contentStoreGen = contentStore?.GenCount; - var mediaStoreGen = mediaStore?.GenCount; - var contentStoreSnap = contentStore?.SnapCount; - var mediaStoreSnap = mediaStore?.SnapCount; - var contentStoreCount = contentStore?.Count; - var mediaStoreCount = mediaStore?.Count; - - var contentStoreCountPlural = contentStoreCount > 1 ? "s" : string.Empty; - var contentStoreGenPlural = contentStoreGen > 1 ? "s" : string.Empty; - var contentStoreSnapPlural = contentStoreSnap > 1 ? "s" : string.Empty; - var mediaStoreCountPlural = mediaStoreCount > 1 ? "s" : string.Empty; - var mediaStoreGenPlural = mediaStoreGen > 1 ? "s" : string.Empty; - var mediaStoreSnapPlural = mediaStoreSnap > 1 ? "s" : string.Empty; - - return - $" Database cache is {dbCacheIsOk}. ContentStore contains {contentStoreCount} item{contentStoreCountPlural} and has {contentStoreGen} generation{contentStoreGenPlural} and {contentStoreSnap} snapshot{contentStoreSnapPlural}. MediaStore contains {mediaStoreCount} item{mediaStoreCountPlural} and has {mediaStoreGen} generation{mediaStoreGenPlural} and {mediaStoreSnap} snapshot{mediaStoreSnapPlural}."; - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/Snap/GenObj.cs b/src/Umbraco.PublishedCache.NuCache/Snap/GenObj.cs deleted file mode 100644 index b294b3234f..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Snap/GenObj.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.Snap; - -internal class GenObj -{ - public readonly long Gen; - public readonly WeakReference WeakGenRef; - public int Count; - - public GenObj(long gen) - { - Gen = gen; - WeakGenRef = new WeakReference(null); - } - - public GenRef GetGenRef() - { - // not thread-safe but always invoked from within a lock - var genRef = (GenRef?)WeakGenRef.Target; - if (genRef == null) - { - WeakGenRef.Target = genRef = new GenRef(this); - } - - return genRef; - } - - public void Reference() => Interlocked.Increment(ref Count); - - public void Release() => Interlocked.Decrement(ref Count); -} diff --git a/src/Umbraco.PublishedCache.NuCache/Snap/GenRef.cs b/src/Umbraco.PublishedCache.NuCache/Snap/GenRef.cs deleted file mode 100644 index b7e8b0a58d..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Snap/GenRef.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.Snap; - -internal class GenRef -{ - public readonly GenObj GenObj; - - public GenRef(GenObj genObj) => GenObj = genObj; - - public long Gen => GenObj.Gen; -} diff --git a/src/Umbraco.PublishedCache.NuCache/Snap/LinkedNode.cs b/src/Umbraco.PublishedCache.NuCache/Snap/LinkedNode.cs deleted file mode 100644 index 3c5a66f212..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Snap/LinkedNode.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.PublishedCache.Snap; - -// NOTE: This cannot be struct because it references itself - -/// -/// Used to represent an item in a linked list -/// -/// -internal class LinkedNode - where TValue : class? -{ - public readonly long Gen; - public volatile LinkedNode? Next; - - // reading & writing references is thread-safe on all .NET platforms - // mark as volatile to ensure we always read the correct value - public volatile TValue? Value; - - public LinkedNode(TValue? value, long gen, LinkedNode? next = null) - { - Value = value; // This is allowed to be null, we actually explicitly set this to null in ClearLocked - Gen = gen; - Next = next; - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/SnapDictionary.cs b/src/Umbraco.PublishedCache.NuCache/SnapDictionary.cs deleted file mode 100644 index b6c87e22bb..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/SnapDictionary.cs +++ /dev/null @@ -1,677 +0,0 @@ -using System.Collections.Concurrent; -using Umbraco.Cms.Core.Exceptions; -using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Infrastructure.PublishedCache.Snap; - -namespace Umbraco.Cms.Infrastructure.PublishedCache; - -public class SnapDictionary - where TValue : class - where TKey : notnull -{ - private static readonly TimeSpan _monitorTimeout = TimeSpan.FromSeconds(30); - - // minGenDelta to be adjusted - // we may want to throttle collects even if delta is reached - // we may want to force collect if delta is not reached but very old - // we may want to adjust delta depending on the number of changes - private const long CollectMinGenDelta = 4; - - private readonly ConcurrentQueue _genObjs; - - // read - // http://www.codeproject.com/Articles/548406/Dictionary-plus-Locking-versus-ConcurrentDictionar - // http://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/ - // http://blogs.msdn.com/b/pfxteam/archive/2011/04/02/10149222.aspx - - // Set, Clear and GetSnapshot have to be protected by a lock - // This class is optimized for many readers, few writers - // Readers are lock-free - - // NOTE - we used to lock _rlocko the long hand way with Monitor.Enter(_rlocko, ref lockTaken) but this has - // been replaced with a normal c# lock because that's exactly how the normal c# lock works, - // see https://blogs.msdn.microsoft.com/ericlippert/2009/03/06/locks-and-exceptions-do-not-mix/ - // for the readlock, there's no reason here to use the long hand way. - private readonly ConcurrentDictionary> _items; - private readonly object _rlocko = new(); - private readonly object _wlocko = new(); - private Task? _collectTask; - private GenObj? _genObj; - private long _liveGen; - private long _floorGen; - private bool _nextGen; - private bool _collectAuto; - - #region Ctor - - public SnapDictionary() - { - _items = new ConcurrentDictionary>(); - _genObjs = new ConcurrentQueue(); - _genObj = null; // no initial gen exists - _liveGen = _floorGen = 0; - _nextGen = false; // first time, must create a snapshot - _collectAuto = true; // collect automatically by default - } - - #endregion - - #region Classes - - public class Snapshot : IDisposable - { - private readonly long _gen; // copied for perfs - private readonly GenRef? _genRef; - private readonly SnapDictionary _store; - private int _disposed; - - // private static int _count; - // private readonly int _thisCount; - internal Snapshot(SnapDictionary store, GenRef genRef) - { - _store = store; - _genRef = genRef; - _gen = genRef.GenObj.Gen; - _genRef.GenObj.Reference(); - - // _thisCount = _count++; - } - - internal Snapshot(SnapDictionary store, long gen) - { - _store = store; - _gen = gen; - } - - public bool IsEmpty - { - get - { - EnsureNotDisposed(); - return _store.IsEmpty(_gen); - } - } - - public long Gen - { - get - { - EnsureNotDisposed(); - return _gen; - } - } - - public void Dispose() - { - if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) - { - return; - } - - _genRef?.GenObj.Release(); - GC.SuppressFinalize(this); - } - - private void EnsureNotDisposed() - { - if (_disposed > 0) - { - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); - } - } - - public TValue? Get(TKey key) - { - EnsureNotDisposed(); - return _store.Get(key, _gen); - } - - public IEnumerable GetAll() - { - EnsureNotDisposed(); - return _store.GetAll(_gen); - } - } - - #endregion - - #region Locking - - // read and write locks are not exclusive - // it is not possible to write-lock while someone is read-locked - // it is possible to read-lock while someone is write-locked - // - // so when getting a read-lock, - // either we are write-locked or not, but if not, we won't be write-locked - // on the other hand the write-lock may be released in the meantime - - // Lock has a 'forceGen' parameter: - // used to start a set of changes that may not commit, to isolate the set from any pending - // changes that would not have been snapshotted yet, so they cannot be rolled back by accident - // - // Release has a 'commit' parameter: - // if false, the live gen is scrapped and changes that have been applied as part of the lock - // are all ignored - Release is private and meant to be invoked with 'commit' being false only - // only on the outermost lock (by SnapDictionaryWriter) - - // side note - using (...) {} for locking is prone to nasty leaks in case of weird exceptions - // such as thread-abort or out-of-memory, which is why we've moved away from the old using wrapper we had on locking. - private readonly string _instanceId = Guid.NewGuid().ToString("N"); - - private class WriteLockInfo - { - public bool Taken; - } - - // a scope contextual that represents a locked writer to the dictionary - private class ScopedWriteLock : ScopeContextualBase - { - private readonly SnapDictionary _dictionary; - private readonly WriteLockInfo _lockinfo = new(); - - public ScopedWriteLock(SnapDictionary dictionary, bool scoped) - { - _dictionary = dictionary; - dictionary.Lock(_lockinfo, scoped); - } - - public override void Release(bool completed) => _dictionary.Release(_lockinfo, completed); - } - - // gets a scope contextual representing a locked writer to the dictionary - // the dict is write-locked until the write-lock is released - // which happens when it is disposed (non-scoped) - // or when the scope context exits (scoped) - public IDisposable? GetScopedWriteLock(ICoreScopeProvider scopeProvider) => - ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); - - private void EnsureLocked() - { - if (!Monitor.IsEntered(_wlocko)) - { - throw new InvalidOperationException("Write lock must be acquried."); - } - } - - private void Lock(WriteLockInfo lockInfo, bool forceGen = false) - { - if (Monitor.IsEntered(_wlocko)) - { - throw new InvalidOperationException("Recursive locks not allowed"); - } - - Monitor.TryEnter(_wlocko, _monitorTimeout, ref lockInfo.Taken); - - if (Monitor.IsEntered(_wlocko) is false) - { - throw new TimeoutException("Could not enter the monitor before timeout in SnapDictionary"); - } - - lock (_rlocko) - { - // assume everything in finally runs atomically - // http://stackoverflow.com/questions/18501678/can-this-unexpected-behavior-of-prepareconstrainedregions-and-thread-abort-be-ex - // http://joeduffyblog.com/2005/03/18/atomicity-and-asynchronous-exception-failures/ - // http://joeduffyblog.com/2007/02/07/introducing-the-new-readerwriterlockslim-in-orcas/ - // http://chabster.blogspot.fr/2013/12/readerwriterlockslim-fails-on-dual.html - // RuntimeHelpers.PrepareConstrainedRegions(); - try - { - } - finally - { - if (_nextGen == false || forceGen) - { - // because we are changing things, a new generation - // is created, which will trigger a new snapshot - if (_nextGen) - { - _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); - } - - _liveGen += 1; - _nextGen = true; // this is the ONLY place where _nextGen becomes true - } - } - } - } - - private void Release(WriteLockInfo lockInfo, bool commit = true) - { - // if the lock wasn't taken in the first place, do nothing - if (!lockInfo.Taken) - { - return; - } - - if (commit == false) - { - lock (_rlocko) - { - try - { - } - finally - { - // forget about the temp. liveGen - _nextGen = false; - _liveGen -= 1; - } - } - - foreach (KeyValuePair> item in _items) - { - LinkedNode? link = item.Value; - if (link.Gen <= _liveGen) - { - continue; - } - - TKey key = item.Key; - if (link.Next == null) - { - _items.TryRemove(key, out link); - } - else - { - _items.TryUpdate(key, link.Next, link); - } - } - } - - // TODO: Shouldn't this be in a finally block? - Monitor.Exit(_wlocko); - } - - #endregion - - #region Set, Clear, Get, Has - - public int Count => _items.Count; - - private LinkedNode? GetHead(TKey key) - { - _items.TryGetValue(key, out LinkedNode? link); // else null - return link; - } - - public void SetLocked(TKey key, TValue? value) - { - EnsureLocked(); - - // this is safe only because we're write-locked - LinkedNode? link = GetHead(key); - if (link != null) - { - // already in the dict - if (link.Gen != _liveGen) - { - // for an older gen - if value is different then insert a new - // link for the new gen, with the new value - if (link.Value != value) - { - _items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link); - } - } - else - { - // for the live gen - we can fix the live gen - and remove it - // if value is null and there's no next gen - if (value == null && link.Next == null) - { - _items.TryRemove(key, out link); - } - else - { - link.Value = value; - } - } - } - else - { - _items.TryAdd(key, new LinkedNode(value, _liveGen)); - } - } - - public void ClearLocked(TKey key) => SetLocked(key, null); - - public void ClearLocked() - { - EnsureLocked(); - - // this is safe only because we're write-locked - foreach (KeyValuePair> kvp in _items.Where(x => x.Value != null)) - { - if (kvp.Value.Gen < _liveGen) - { - var link = new LinkedNode(null, _liveGen, kvp.Value); - _items.TryUpdate(kvp.Key, link, kvp.Value); - } - else - { - kvp.Value.Value = null; - } - } - } - - public TValue? Get(TKey key, long gen) - { - // look ma, no lock! - LinkedNode? link = GetHead(key); - while (link != null) - { - if (link.Gen <= gen) - { - return link.Value; // may be null - } - - link = link.Next; - } - - return null; - } - - public IEnumerable GetAll(long gen) - { - // enumerating on .Values locks the concurrent dictionary, - // so better get a shallow clone in an array and release - LinkedNode[] links = _items.Values.ToArray(); - foreach (LinkedNode l in links) - { - LinkedNode? link = l; - while (link != null) - { - if (link.Gen <= gen) - { - if (link.Value != null) - { - yield return link.Value; - } - - break; - } - - link = link.Next; - } - } - } - - public bool IsEmpty(long gen) - { - var has = _items.Any(x => - { - LinkedNode? link = x.Value; - while (link != null) - { - if (link.Gen <= gen && link.Value != null) - { - return true; - } - - link = link.Next; - } - - return false; - }); - return has == false; - } - - #endregion - - #region Snapshots - - public Snapshot CreateSnapshot() - { - lock (_rlocko) - { - // if no next generation is required, and we already have a gen object, - // use it to create a new snapshot - if (_nextGen == false && _genObj != null) - { - return new Snapshot(this, _genObj.GetGenRef()); - } - - // else we need to try to create a new gen object - // whether we are wlocked or not, noone can rlock while we do, - // so _liveGen and _nextGen are safe - if (Monitor.IsEntered(_wlocko)) - { - // write-locked, cannot use latest gen (at least 1) so use previous - var snapGen = _nextGen ? _liveGen - 1 : _liveGen; - - // create a new gen object if we don't already have one - // (happens the first time a snapshot is created) - if (_genObj == null) - { - _genObjs.Enqueue(_genObj = new GenObj(snapGen)); - } - - // if we have one already, ensure it's consistent - else if (_genObj.Gen != snapGen) - { - throw new PanicException( - $"The generation {_genObj.Gen} does not equal the snapshot generation {snapGen}"); - } - } - else - { - // not write-locked, can use latest gen (_liveGen), create a corresponding new gen object - _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); - _nextGen = false; // this is the ONLY thing that triggers a _liveGen++ - } - - // so... - // the genObj has a weak ref to the genRef, and is queued - // the snapshot has a ref to the genRef, which has a ref to the genObj - // when the snapshot is disposed, it decreases genObj counter - // so after a while, one of these conditions is going to be true: - // - genObj.Count is zero because all snapshots have properly been disposed - // - genObj.WeakGenRef is dead because all snapshots have been collected - // in both cases, we will dequeue and collect - var snapshot = new Snapshot(this, _genObj.GetGenRef()); - - // reading _floorGen is safe if _collectTask is null - if (_collectTask == null && _collectAuto && _liveGen - _floorGen > CollectMinGenDelta) - { - CollectAsyncLocked(); - } - - return snapshot; - } - } - - public Task CollectAsync() - { - lock (_rlocko) - { - return CollectAsyncLocked(); - } - } - - private Task CollectAsyncLocked() - { - if (_collectTask != null) - { - return _collectTask; - } - - // ReSharper disable InconsistentlySynchronizedField - Task task = _collectTask = Task.Run(() => Collect()); - _collectTask.ContinueWith( - _ => - { - lock (_rlocko) - { - _collectTask = null; - } - }, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default); - - // ReSharper restore InconsistentlySynchronizedField - return task; - } - - private void Collect() - { - // see notes in CreateSnapshot - while (_genObjs.TryPeek(out GenObj? genObj) && (genObj.Count == 0 || genObj.WeakGenRef.IsAlive == false)) - { - _genObjs.TryDequeue(out genObj); // cannot fail since TryPeek has succeeded - _floorGen = genObj!.Gen; - } - - Collect(_items); - } - - private void Collect(ConcurrentDictionary> dict) - { - // it is OK to enumerate a concurrent dictionary and it does not lock - // it - and here it's not an issue if we skip some items, they will be - // processed next time we collect - long liveGen; - - // r is good - lock (_rlocko) - { - liveGen = _liveGen; - if (_nextGen == false) - { - liveGen += 1; - } - } - - // Console.WriteLine("Collect live=" + liveGen + " floor=" + _floorGen); - foreach (KeyValuePair> kvp in dict) - { - LinkedNode? link = kvp.Value; - - // Console.WriteLine("Collect id=" + kvp.Key + " gen=" + link.Gen - // + " nxt=" + (link.Next == null ? null : "next") - // + " val=" + link.Value); - - // reasons to collect the head: - // gen must be < liveGen (we never collect live gen) - // next == null && value == null (we have no data at all) - // next != null && value == null BUT gen > floor (noone wants us) - // not live means .Next and .Value are safe - if (link.Gen < liveGen && link.Value == null - && (link.Next == null || link.Gen <= _floorGen)) - { - // not live, null value, no next link = remove that one -- but only if - // the dict has not been updated, have to do it via ICollection<> (thanks - // Mr Toub) -- and if the dict has been updated there is nothing to collect - var idict = dict as ICollection>>; - /*var removed =*/ - idict.Remove(kvp); - - // Console.WriteLine("remove (" + (removed ? "true" : "false") + ")"); - continue; - } - - // in any other case we're not collecting the head, we need to go to Next - // and if there is no Next, skip - if (link.Next == null) - { - continue; - } - - // else go to Next and loop while above floor, and kill everything below - while (link.Next != null && link.Next.Gen > _floorGen) - { - link = link.Next; - } - - link.Next = null; - } - } - - // TODO: This is never used? Should it be? Maybe move to TestHelper below? - // public /*async*/ Task PendingCollect() - // { - // Task task; - // lock (_rlocko) - // { - // task = _collectTask; - // } - // return task ?? Task.CompletedTask; - // //if (task != null) - // // await task; - // } - public long GenCount => _genObjs.Count; - - public long SnapCount => _genObjs.Sum(x => x.Count); - - #endregion - - #region Unit testing - - private TestHelper? _unitTesting; - - // note: nothing here is thread-safe - internal class TestHelper - { - private readonly SnapDictionary _dict; - - public TestHelper(SnapDictionary dict) => _dict = dict; - - public long LiveGen => _dict._liveGen; - - public long FloorGen => _dict._floorGen; - - public bool NextGen => _dict._nextGen; - - public bool IsLocked => Monitor.IsEntered(_dict._wlocko); - - public bool CollectAuto - { - get => _dict._collectAuto; - set => _dict._collectAuto = value; - } - - public GenObj? GenObj => _dict._genObj; - - public ConcurrentQueue GenObjs => _dict._genObjs; - - public Snapshot LiveSnapshot => new(_dict, _dict._liveGen); - - public GenVal[] GetValues(TKey key) - { - _dict._items.TryGetValue(key, out LinkedNode? link); // else null - - if (link == null) - { - return new GenVal[0]; - } - - var genVals = new List(); - do - { - genVals.Add(new GenVal(link.Gen, link.Value)); - link = link.Next; - } - while (link != null); - - return genVals.ToArray(); - } - - public class GenVal - { - public GenVal(long gen, TValue? value) - { - Gen = gen; - Value = value; - } - - public long Gen { get; } - public TValue? Value { get; } - } - } - - internal TestHelper Test => _unitTesting ?? (_unitTesting = new TestHelper(this)); - - #endregion -} diff --git a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj deleted file mode 100644 index 04986712c8..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - Umbraco.Cms.PublishedCache.NuCache - Umbraco CMS - Published cache - NuCache - Contains the published cache assembly needed to run Umbraco CMS. - Umbraco.Cms.Infrastructure.PublishedCache - - - - false - - - - - - - - - - - - - - <_Parameter1>Umbraco.Tests - - - <_Parameter1>Umbraco.Tests.UnitTests - - - <_Parameter1>Umbraco.Tests.Integration - - - <_Parameter1>Umbraco.Tests.Benchmarks - - - <_Parameter1>DynamicProxyGenAssembly2 - - - diff --git a/src/Umbraco.PublishedCache.NuCache/readme.md b/src/Umbraco.PublishedCache.NuCache/readme.md deleted file mode 100644 index e97ab0d561..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/readme.md +++ /dev/null @@ -1,120 +0,0 @@ -NuCache Documentation -====================== - -HOW IT WORKS -------------- - -NuCache uses a ContentStore to keep content - basically, a dictionary of int => content, -and some logic to maintain it up-to-date. In order to provide immutable content to -pages rendering, a ContentStore can create ContentViews. A ContentView basically is -another int => content dictionary, containing entries only for things that have changed -in the ContentStore - so the ContentStore changes, but it updates the views so that -they - -Views are chained, ie each new view is the parent of previously existing views. A view -knows its parent but not the other way round, so views just disappear when they are GC. - -When reading the cache, we read views up the chain until we find a value (which may be -null) for the given id, and finally we read the store itself. - - -The PublishedSnapshotService manages a ContentStore for content, and another for media. -When a PublishedSnapshot is created, the PublishedSnapshotService gets ContentView objects from the stores. -Views provide an immutable snapshot of the content and media. - -When the PublishedSnapshotService is notified of changes, it notifies the stores. -Then it resync the current PublishedSnapshot, so that it requires new views, etc. - -Whenever a content, media or member is modified or removed, the cmsContentNu table -is updated with a json dictionary of alias => property value, so that a content, -media or member can be loaded with one database row - this is what is used to populate -the in-memory cache. - - -A ContentStore actually stores ContentNode instances, which contain what's common to -both the published and draft version of content + the actual published and/or draft -content. - - -LOCKS ------- - -Each ContentStore is protected by a reader/writer lock 'Locker' that is used both by -the store and its views to ensure that the store remains consistent. - -Each ContentStore has a _freezeLock object used to protect the 'Frozen' -state of the store. It's a disposable object that releases the lock when disposed, -so usage would be: using (store.Frozen) { ... }. - -The PublishedSnapshotService has a _storesLock object used to guarantee atomic access to the -set of content, media stores. - - -CACHE ------- - -For each set of views, the PublishedSnapshotService creates a SnapshotCache. So a SnapshotCache -is valid until anything changes in the content or media trees. In other words, things -that go in the SnapshotCache stay until a content or media is modified. - -For each PublishedSnapshot, the PublishedSnapshotService creates a PublishedSnapshotCache. So a PublishedSnapshotCache is valid -for the duration of the PublishedSnapshot (usually, the request). In other words, things that go -in the PublishedSnapshotCache stay (and are visible to) for the duration of the request only. - -The PublishedSnapshotService defines a static constant FullCacheWhenPreviewing, that defines -how caches operate when previewing: -- when true, the caches in preview mode work normally. -- when false, everything that would go to the SnapshotCache goes to the PublishedSnapshotCache. -At the moment it is true in the code, which means that eg converted values for -previewed content will go in the SnapshotCache. Makes for faster preview, but uses -more memory on the long term... would need some benchmarking to figure out what is -best. - -Members only live for the duration of the PublishedSnapshot. So, for members SnapshotCache is -never used, and everything goes to the PublishedSnapshotCache. - -All cache keys are computed in the CacheKeys static class. - - -TESTS ------ - -For testing purposes the following mechanisms exist: - -The PublishedSnapshot type has a static Current property that is used to obtain the 'current' -PublishedSnapshot in many places, going through the PublishedCachesServiceResolver to get the -current service, and asking the current service for the current PublishedSnapshot, which by -default relies on UmbracoContext. For test purposes, it is possible to override the -entire mechanism by defining PublishedSnapshot.GetCurrentPublishedSnapshotFunc which should return a PublishedSnapshot. - -A PublishedContent keeps only id-references to its parent and children, and needs a -way to retrieve the actual objects from the cache - which depends on the current -PublishedSnapshot. It is possible to override the entire mechanism by defining PublishedContent. -GetContentByIdFunc or .GetMediaByIdFunc. - -Setting these functions must be done before Resolution is frozen. - - -STATUS ------- - -"Detached" contents & properties, which need to be refactored anyway, are not supported -by NuCache - throwing NotImplemented in ContentCache. - -All the cached elements rely on guids for the cache key, and not ints, so it could be -possible to support detached contents & properties, even those that do not have an actual -int id, but again this should be refactored entirely anyway. - -Not doing any row-version checks (see XmlStore) when reloading from database, though it -is maintained in the database. Two TODO in PublishedSnapshotService. Should we do it? - -There is no on-disk cache at all so everything is reloaded from the cmsContentNu table -when the site restarts. This is pretty fast, but we should experiment with solutions to -store things locally (and deal with the sync issues, see XmlStore...). - -Doing our best with PublishedMember but the whole thing should be refactored, because -PublishedMember exposes properties that IPublishedContent does not, and that are going -to be lost soon as the member is wrapped (content set, model...) - so we probably need -some sort of IPublishedMember. - -/ diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs index 09ff520766..905a4b5a28 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs @@ -45,9 +45,9 @@ public static partial class UmbracoBuilderExtensions factory.GetRequiredService(), factory.GetRequiredService(), factory.GetRequiredService(), - factory.GetRequiredService(), factory.GetRequiredService(), - factory.GetRequiredService())) + factory.GetRequiredService(), + factory.GetRequiredService())) .AddRoleStore() .AddRoleManager() .AddMemberManager() diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs index 9bd6f8c599..858c7d82cd 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Web; namespace Umbraco.Extensions; @@ -18,6 +19,12 @@ public static class FriendlyPublishedContentExtensions private static IVariationContextAccessor VariationContextAccessor { get; } = StaticServiceProvider.Instance.GetRequiredService(); + private static IPublishedContentCache PublishedContentCache { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IDocumentNavigationQueryService DocumentNavigationQueryService { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + private static IPublishedModelFactory PublishedModelFactory { get; } = StaticServiceProvider.Instance.GetRequiredService(); @@ -48,19 +55,6 @@ public static class FriendlyPublishedContentExtensions private static IPublishedValueFallback PublishedValueFallback { get; } = StaticServiceProvider.Instance.GetRequiredService(); - private static IPublishedSnapshot? PublishedSnapshot - { - get - { - if (!UmbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) - { - return null; - } - - return umbracoContext.PublishedSnapshot; - } - } - private static IMediaTypeService MediaTypeService { get; } = StaticServiceProvider.Instance.GetRequiredService(); @@ -200,6 +194,108 @@ public static class FriendlyPublishedContentExtensions public static T? Value(this IPublishedContent content, string alias, string? culture = null, string? segment = null, Fallback fallback = default, T? defaultValue = default) => content.Value(PublishedValueFallback, alias, culture, segment, fallback, defaultValue); + /// + /// Gets the root content (ancestor or self at level 1) for the specified . + /// + /// The content. + /// + /// The root content (ancestor or self at level 1) for the specified . + /// + /// + /// This is the same as calling + /// with maxLevel + /// set to 1. + /// + public static IPublishedContent Root(this IPublishedContent content) + => content.Root(PublishedContentCache, DocumentNavigationQueryService); + + /// + /// Gets the root content (ancestor or self at level 1) for the specified if it's of the + /// specified content type . + /// + /// The content type. + /// The content. + /// + /// The root content (ancestor or self at level 1) for the specified of content type + /// . + /// + /// + /// This is the same as calling + /// with + /// maxLevel set to 1. + /// + public static T? Root(this IPublishedContent content) + where T : class, IPublishedContent + => content.Root(PublishedContentCache, DocumentNavigationQueryService); + + /// + /// Gets the parent of the content item. + /// + /// The content. + /// The content type. + /// The parent of content of the specified content type or null. + public static T? Parent(this IPublishedContent content) + where T : class, IPublishedContent + => content.Parent(PublishedContentCache, DocumentNavigationQueryService); + + /// + /// Gets the ancestors of the content. + /// + /// The content. + /// The ancestors of the content, in down-top order. + /// Does not consider the content itself. + public static IEnumerable Ancestors(this IPublishedContent content) + => content.Ancestors(PublishedContentCache, DocumentNavigationQueryService); + + /// + /// Gets the content and its ancestors. + /// + /// The content. + /// The content and its ancestors, in down-top order. + public static IEnumerable AncestorsOrSelf(this IPublishedContent content) + => content.AncestorsOrSelf(PublishedContentCache, DocumentNavigationQueryService); + + /// + /// Gets the content and its ancestors, of a specified content type. + /// + /// The content type. + /// The content. + /// The content and its ancestors, of the specified content type, in down-top order. + /// May or may not begin with the content itself, depending on its content type. + public static IEnumerable AncestorsOrSelf(this IPublishedContent content) + where T : class, IPublishedContent + => content.AncestorsOrSelf(PublishedContentCache, DocumentNavigationQueryService); + + /// + /// Gets the ancestor of the content, i.e. its parent. + /// + /// The content. + /// The ancestor of the content. + public static IPublishedContent? Ancestor(this IPublishedContent content) + => content.Ancestor(PublishedContentCache, DocumentNavigationQueryService); + + /// + /// Gets the nearest ancestor of the content, of a specified content type. + /// + /// The content type. + /// The content. + /// The nearest (in down-top order) ancestor of the content, of the specified content type. + /// Does not consider the content itself. May return null. + public static T? Ancestor(this IPublishedContent content) + where T : class, IPublishedContent + => content.Ancestor(PublishedContentCache, DocumentNavigationQueryService); + + /// + /// Gets the content or its nearest ancestor, of a specified content type. + /// + /// The content type. + /// The content. + /// The content or its nearest (in down-top order) ancestor, of the specified content type. + /// May or may not return the content itself depending on its content type. May return null. + public static T? AncestorOrSelf(this IPublishedContent content) + where T : class, IPublishedContent + => content.AncestorOrSelf(PublishedContentCache, DocumentNavigationQueryService); + /// /// Returns all DescendantsOrSelf of all content referenced /// @@ -215,7 +311,7 @@ public static class FriendlyPublishedContentExtensions /// public static IEnumerable DescendantsOrSelfOfType( this IEnumerable parentNodes, string docTypeAlias, string? culture = null) - => parentNodes.DescendantsOrSelfOfType(VariationContextAccessor, docTypeAlias, culture); + => parentNodes.DescendantsOrSelfOfType(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, docTypeAlias, culture); /// /// Returns all DescendantsOrSelf of all content referenced @@ -233,77 +329,77 @@ public static class FriendlyPublishedContentExtensions this IEnumerable parentNodes, string? culture = null) where T : class, IPublishedContent - => parentNodes.DescendantsOrSelf(VariationContextAccessor, culture); + => parentNodes.DescendantsOrSelf(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); public static IEnumerable Descendants(this IPublishedContent content, string? culture = null) - => content.Descendants(VariationContextAccessor, culture); + => content.Descendants(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); public static IEnumerable Descendants(this IPublishedContent content, int level, string? culture = null) - => content.Descendants(VariationContextAccessor, level, culture); + => content.Descendants(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, level, culture); public static IEnumerable DescendantsOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.DescendantsOfType(VariationContextAccessor, contentTypeAlias, culture); + => content.DescendantsOfType(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, contentTypeAlias, culture); public static IEnumerable Descendants(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.Descendants(VariationContextAccessor, culture); + => content.Descendants(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); public static IEnumerable Descendants(this IPublishedContent content, int level, string? culture = null) where T : class, IPublishedContent - => content.Descendants(VariationContextAccessor, level, culture); + => content.Descendants(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, level, culture); public static IEnumerable DescendantsOrSelf( this IPublishedContent content, string? culture = null) - => content.DescendantsOrSelf(VariationContextAccessor, culture); + => content.DescendantsOrSelf(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level, string? culture = null) - => content.DescendantsOrSelf(VariationContextAccessor, level, culture); + => content.DescendantsOrSelf(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, level, culture); public static IEnumerable DescendantsOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.DescendantsOrSelfOfType(VariationContextAccessor, contentTypeAlias, culture); + => content.DescendantsOrSelfOfType(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, contentTypeAlias, culture); public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.DescendantsOrSelf(VariationContextAccessor, culture); + => content.DescendantsOrSelf(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level, string? culture = null) where T : class, IPublishedContent - => content.DescendantsOrSelf(VariationContextAccessor, level, culture); + => content.DescendantsOrSelf(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, level, culture); public static IPublishedContent? Descendant(this IPublishedContent content, string? culture = null) - => content.Descendant(VariationContextAccessor, culture); + => content.Descendant(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); public static IPublishedContent? Descendant(this IPublishedContent content, int level, string? culture = null) - => content.Descendant(VariationContextAccessor, level, culture); + => content.Descendant(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, level, culture); public static IPublishedContent? DescendantOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.DescendantOfType(VariationContextAccessor, contentTypeAlias, culture); + => content.DescendantOfType(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, contentTypeAlias, culture); public static T? Descendant(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.Descendant(VariationContextAccessor, culture); + => content.Descendant(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); public static T? Descendant(this IPublishedContent content, int level, string? culture = null) where T : class, IPublishedContent - => content.Descendant(VariationContextAccessor, level, culture); + => content.Descendant(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, level, culture); public static IPublishedContent DescendantOrSelf(this IPublishedContent content, string? culture = null) => content.DescendantOrSelf(VariationContextAccessor, culture); public static IPublishedContent? DescendantOrSelf(this IPublishedContent content, int level, string? culture = null) - => content.DescendantOrSelf(VariationContextAccessor, level, culture); + => content.DescendantOrSelf(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, level, culture); public static IPublishedContent? DescendantOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.DescendantOrSelfOfType(VariationContextAccessor, contentTypeAlias, culture); + => content.DescendantOrSelfOfType(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, contentTypeAlias, culture); public static T? DescendantOrSelf(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.DescendantOrSelf(VariationContextAccessor, culture); + => content.DescendantOrSelf(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); public static T? DescendantOrSelf(this IPublishedContent content, int level, string? culture = null) where T : class, IPublishedContent - => content.DescendantOrSelf(VariationContextAccessor, level, culture); + => content.DescendantOrSelf(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, level, culture); /// /// Gets the children of the content item. @@ -331,7 +427,7 @@ public static class FriendlyPublishedContentExtensions /// /// public static IEnumerable Children(this IPublishedContent content, string? culture = null) - => content.Children(VariationContextAccessor, culture); + => content.Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); /// /// Gets the children of the content, filtered by a predicate. @@ -350,7 +446,7 @@ public static class FriendlyPublishedContentExtensions this IPublishedContent content, Func predicate, string? culture = null) - => content.Children(VariationContextAccessor, predicate, culture); + => content.Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, predicate, culture); /// /// Gets the children of the content, of any of the specified types. @@ -363,7 +459,7 @@ public static class FriendlyPublishedContentExtensions /// The content type alias. /// The children of the content, of any of the specified types. public static IEnumerable? ChildrenOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.ChildrenOfType(VariationContextAccessor, contentTypeAlias, culture); + => content.ChildrenOfType(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, contentTypeAlias, culture); /// /// Gets the children of the content, of a given content type. @@ -380,30 +476,30 @@ public static class FriendlyPublishedContentExtensions /// public static IEnumerable? Children(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.Children(VariationContextAccessor, culture); + => content.Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); public static IPublishedContent? FirstChild(this IPublishedContent content, string? culture = null) - => content.FirstChild(VariationContextAccessor, culture); + => content.FirstChild(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); /// /// Gets the first child of the content, of a given content type. /// public static IPublishedContent? FirstChildOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.FirstChildOfType(VariationContextAccessor, contentTypeAlias, culture); + => content.FirstChildOfType(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, contentTypeAlias, culture); public static IPublishedContent? FirstChild(this IPublishedContent content, Func predicate, string? culture = null) - => content.FirstChild(VariationContextAccessor, predicate, culture); + => content.FirstChild(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, predicate, culture); public static IPublishedContent? FirstChild(this IPublishedContent content, Guid uniqueId, string? culture = null) - => content.FirstChild(VariationContextAccessor, uniqueId, culture); + => content.FirstChild(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, uniqueId, culture); public static T? FirstChild(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.FirstChild(VariationContextAccessor, culture); + => content.FirstChild(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); public static T? FirstChild(this IPublishedContent content, Func predicate, string? culture = null) where T : class, IPublishedContent - => content.FirstChild(VariationContextAccessor, predicate, culture); + => content.FirstChild(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, predicate, culture); /// /// Gets the siblings of the content. @@ -418,7 +514,7 @@ public static class FriendlyPublishedContentExtensions /// Note that in V7 this method also return the content node self. /// public static IEnumerable? Siblings(this IPublishedContent content, string? culture = null) - => content.Siblings(PublishedSnapshot, VariationContextAccessor, culture); + => content.Siblings(PublishedContentCache, DocumentNavigationQueryService, VariationContextAccessor, culture); /// /// Gets the siblings of the content, of a given content type. @@ -434,7 +530,7 @@ public static class FriendlyPublishedContentExtensions /// Note that in V7 this method also return the content node self. /// public static IEnumerable? SiblingsOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.SiblingsOfType(PublishedSnapshot, VariationContextAccessor, contentTypeAlias, culture); + => content.SiblingsOfType(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, contentTypeAlias, culture); /// /// Gets the siblings of the content, of a given content type. @@ -451,7 +547,7 @@ public static class FriendlyPublishedContentExtensions /// public static IEnumerable? Siblings(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.Siblings(PublishedSnapshot, VariationContextAccessor, culture); + => content.Siblings(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); /// /// Gets the siblings of the content including the node itself to indicate the position. @@ -465,7 +561,7 @@ public static class FriendlyPublishedContentExtensions public static IEnumerable? SiblingsAndSelf( this IPublishedContent content, string? culture = null) - => content.SiblingsAndSelf(PublishedSnapshot, VariationContextAccessor, culture); + => content.SiblingsAndSelf(PublishedContentCache, DocumentNavigationQueryService, VariationContextAccessor, culture); /// /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. @@ -481,7 +577,7 @@ public static class FriendlyPublishedContentExtensions this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.SiblingsAndSelfOfType(PublishedSnapshot, VariationContextAccessor, contentTypeAlias, culture); + => content.SiblingsAndSelfOfType(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, contentTypeAlias, culture); /// /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. @@ -495,7 +591,7 @@ public static class FriendlyPublishedContentExtensions /// The siblings of the content including the node itself, of the given content type. public static IEnumerable? SiblingsAndSelf(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.SiblingsAndSelf(PublishedSnapshot, VariationContextAccessor, culture); + => content.SiblingsAndSelf(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService, culture); /// /// Gets the url of the content item. @@ -530,6 +626,8 @@ public static class FriendlyPublishedContentExtensions => content.ChildrenAsTable( VariationContextAccessor, + PublishedContentCache, + DocumentNavigationQueryService, ContentTypeService, MediaTypeService, MemberTypeService, diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index a99893accc..f4898a458a 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -32,7 +32,8 @@ - + + diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs index fe71e62f1f..4c16f4e571 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs @@ -20,7 +20,7 @@ public class UmbracoContext : DisposableObjectSlim, IUmbracoContext private readonly IHostingEnvironment _hostingEnvironment; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IWebProfilerService _webProfilerService; - private readonly Lazy _publishedSnapshot; + private readonly ICacheManager _cacheManager; private readonly UmbracoRequestPaths _umbracoRequestPaths; private readonly UriUtility _uriUtility; private Uri? _cleanedUmbracoUrl; @@ -34,31 +34,24 @@ public class UmbracoContext : DisposableObjectSlim, IUmbracoContext // otherwise it's used by EnsureContext above // warn: does *not* manage setting any IUmbracoContextAccessor internal UmbracoContext( - IPublishedSnapshotService publishedSnapshotService, UmbracoRequestPaths umbracoRequestPaths, IHostingEnvironment hostingEnvironment, UriUtility uriUtility, ICookieManager cookieManager, IHttpContextAccessor httpContextAccessor, - IWebProfilerService webProfilerService) + IWebProfilerService webProfilerService, + ICacheManager cacheManager) { - if (publishedSnapshotService == null) - { - throw new ArgumentNullException(nameof(publishedSnapshotService)); - } _uriUtility = uriUtility; _hostingEnvironment = hostingEnvironment; _cookieManager = cookieManager; _httpContextAccessor = httpContextAccessor; _webProfilerService = webProfilerService; + _cacheManager = cacheManager; ObjectCreated = DateTime.Now; UmbracoRequestId = Guid.NewGuid(); _umbracoRequestPaths = umbracoRequestPaths; - - // beware - we cannot expect a current user here, so detecting preview mode must be a lazy thing - _publishedSnapshot = - new Lazy(() => publishedSnapshotService.CreatePublishedSnapshot(PreviewToken)); } /// @@ -106,16 +99,13 @@ _originalRequestUrl ??= RequestUrl ?? new Uri("http://localhost"); _cleanedUmbracoUrl ??= _uriUtility.UriToUmbraco(OriginalRequestUrl); /// - public IPublishedSnapshot PublishedSnapshot => _publishedSnapshot.Value; + public IPublishedContentCache Content => _cacheManager.Content; /// - public IPublishedContentCache? Content => PublishedSnapshot.Content; + public IPublishedMediaCache Media => _cacheManager.Media; /// - public IPublishedMediaCache? Media => PublishedSnapshot.Media; - - /// - public IDomainCache? Domains => PublishedSnapshot.Domains; + public IDomainCache Domains => _cacheManager.Domains; /// public IPublishedRequest? PublishedRequest { get; set; } @@ -161,30 +151,6 @@ _cleanedUmbracoUrl ??= _uriUtility.UriToUmbraco(OriginalRequestUrl); private set => _previewing = value; } - /// - public IDisposable ForcedPreview(bool preview) - { - // say we render an RTE in a give 'preview' mode that might not be the 'current' one, - // then due to the way it all works at the moment, the 'current' published snapshot need to be in the proper - // default 'preview' mode - somehow we have to force it. and that could be recursive. - InPreviewMode = preview; - return PublishedSnapshot.ForcedPreview(preview, orig => InPreviewMode = orig); - } - - /// - protected override void DisposeResources() - { - // DisposableObject ensures that this runs only once - - // help caches release resources - // (but don't create caches just to dispose them) - // context is not multi-threaded - if (_publishedSnapshot.IsValueCreated) - { - _publishedSnapshot.Value.Dispose(); - } - } - private void DetectPreviewMode() { if (RequestUrl != null @@ -198,4 +164,9 @@ _cleanedUmbracoUrl ??= _uriUtility.UriToUmbraco(OriginalRequestUrl); _previewing = _previewToken.IsNullOrWhiteSpace() == false; } + + // TODO: Remove this + protected override void DisposeResources() + { + } } diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs index 2333cf2230..8c9459be37 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs @@ -19,56 +19,34 @@ public class UmbracoContextFactory : IUmbracoContextFactory private readonly IHostingEnvironment _hostingEnvironment; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IWebProfilerService _webProfilerService; - private readonly IPublishedSnapshotService _publishedSnapshotService; + private readonly ICacheManager _cacheManager; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly UmbracoRequestPaths _umbracoRequestPaths; private readonly UriUtility _uriUtility; - [Obsolete("Use non-obsolete ctor. This will be removed in Umbraco 15.")] - public UmbracoContextFactory( - IUmbracoContextAccessor umbracoContextAccessor, - IPublishedSnapshotService publishedSnapshotService, - UmbracoRequestPaths umbracoRequestPaths, - IHostingEnvironment hostingEnvironment, - UriUtility uriUtility, - ICookieManager cookieManager, - IHttpContextAccessor httpContextAccessor) - :this( - umbracoContextAccessor, - publishedSnapshotService, - umbracoRequestPaths, - hostingEnvironment, - uriUtility, - cookieManager, - httpContextAccessor, - StaticServiceProvider.Instance.GetRequiredService()) - { - - } /// /// Initializes a new instance of the class. /// public UmbracoContextFactory( IUmbracoContextAccessor umbracoContextAccessor, - IPublishedSnapshotService publishedSnapshotService, UmbracoRequestPaths umbracoRequestPaths, IHostingEnvironment hostingEnvironment, UriUtility uriUtility, ICookieManager cookieManager, IHttpContextAccessor httpContextAccessor, - IWebProfilerService webProfilerService) + IWebProfilerService webProfilerService, + ICacheManager cacheManager) { _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _publishedSnapshotService = publishedSnapshotService ?? - throw new ArgumentNullException(nameof(publishedSnapshotService)); _umbracoRequestPaths = umbracoRequestPaths ?? throw new ArgumentNullException(nameof(umbracoRequestPaths)); _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); _uriUtility = uriUtility ?? throw new ArgumentNullException(nameof(uriUtility)); _cookieManager = cookieManager ?? throw new ArgumentNullException(nameof(cookieManager)); _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); _webProfilerService = webProfilerService; + _cacheManager = cacheManager; } /// @@ -86,11 +64,11 @@ public class UmbracoContextFactory : IUmbracoContextFactory } private IUmbracoContext CreateUmbracoContext() => new UmbracoContext( - _publishedSnapshotService, _umbracoRequestPaths, _hostingEnvironment, _uriUtility, _cookieManager, _httpContextAccessor, - _webProfilerService); + _webProfilerService, + _cacheManager); } diff --git a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs index f102cca409..57666cbc4f 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs @@ -1,10 +1,13 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Web.Common.Filters; @@ -20,6 +23,8 @@ public class UmbLoginController : SurfaceController private readonly IMemberManager _memberManager; private readonly IMemberSignInManager _signInManager; private readonly ITwoFactorLoginService _twoFactorLoginService; + private readonly IPublishedContentCache _contentCache; + private readonly IDocumentNavigationQueryService _navigationQueryService; [ActivatorUtilitiesConstructor] public UmbLoginController( @@ -31,12 +36,42 @@ public class UmbLoginController : SurfaceController IPublishedUrlProvider publishedUrlProvider, IMemberSignInManager signInManager, IMemberManager memberManager, - ITwoFactorLoginService twoFactorLoginService) + ITwoFactorLoginService twoFactorLoginService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) { _signInManager = signInManager; _memberManager = memberManager; _twoFactorLoginService = twoFactorLoginService; + _contentCache = contentCache; + _navigationQueryService = navigationQueryService; + } + + [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + public UmbLoginController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManager signInManager, + IMemberManager memberManager, + ITwoFactorLoginService twoFactorLoginService) + : this( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider, + signInManager, + memberManager, + twoFactorLoginService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { } [HttpPost] @@ -67,7 +102,7 @@ public class UmbLoginController : SurfaceController // If it's not a local URL we'll redirect to the root of the current site. return Redirect(Url.IsLocalUrl(model.RedirectUrl) ? model.RedirectUrl - : CurrentPage!.AncestorOrSelf(1)!.Url(PublishedUrlProvider)); + : CurrentPage!.AncestorOrSelf(_contentCache, _navigationQueryService, 1)!.Url(PublishedUrlProvider)); } // Redirect to current URL by default. diff --git a/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs b/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs index 801e4af08f..93e450dfb2 100644 --- a/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs +++ b/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs @@ -171,7 +171,7 @@ public class PublicAccessRequestHandler : IPublicAccessRequestHandler if (pageId != publishedRequest?.PublishedContent?.Id) { IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - IPublishedContent? publishedContent = umbracoContext.PublishedSnapshot.Content?.GetById(pageId); + IPublishedContent? publishedContent = umbracoContext.Content?.GetById(pageId); if (publishedContent is null || publishedRequest is null) { throw new InvalidOperationException("No content found by id " + pageId); diff --git a/tests/Umbraco.TestData/Extensions/UmbracoBuilderExtensions.cs b/tests/Umbraco.TestData/Extensions/UmbracoBuilderExtensions.cs index c695653503..9158f9141b 100644 --- a/tests/Umbraco.TestData/Extensions/UmbracoBuilderExtensions.cs +++ b/tests/Umbraco.TestData/Extensions/UmbracoBuilderExtensions.cs @@ -1,9 +1,7 @@ -using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.TestData.Configuration; @@ -28,10 +26,6 @@ public static class UmbracoBuilderExtensions builder.Services.Configure(testDataSection); - if (config.IgnoreLocalDb) - { - builder.Services.AddSingleton(factory => new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }); - } builder.Services.Configure(options => options.AddFilter(new UmbracoPipelineFilter(nameof(LoadTestController)) diff --git a/tests/Umbraco.Tests.Common/Builders/ContentDataBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentDataBuilder.cs index 8d4778111f..cd0c6f031f 100644 --- a/tests/Umbraco.Tests.Common/Builders/ContentDataBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/ContentDataBuilder.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +using Umbraco.Cms.Infrastructure.HybridCache; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Builders.Interfaces; using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Common.Builders; -public class ContentDataBuilder : BuilderBase, IWithNameBuilder +internal class ContentDataBuilder : BuilderBase, IWithNameBuilder { private Dictionary _cultureInfos; private string _name; diff --git a/tests/Umbraco.Tests.Common/Builders/ContentNodeKitBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentNodeKitBuilder.cs index c58968a655..cbdc89b42e 100644 --- a/tests/Umbraco.Tests.Common/Builders/ContentNodeKitBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/ContentNodeKitBuilder.cs @@ -1,95 +1,95 @@ -using System; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -namespace Umbraco.Cms.Tests.Common.Builders; - -public class ContentNodeKitBuilder : BuilderBase -{ - private ContentNode _contentNode; - private int _contentTypeId; - private ContentData _draftData; - private ContentData _publishedData; - - public ContentNodeKitBuilder WithContentNode(ContentNode contentNode) - { - _contentNode = contentNode; - return this; - } - - public ContentNodeKitBuilder WithContentNode(int id, Guid uid, int level, string path, int sortOrder, int parentContentId, DateTime createDate, int creatorId) - { - _contentNode = new ContentNode(id, uid, level, path, sortOrder, parentContentId, createDate, creatorId); - return this; - } - - public ContentNodeKitBuilder WithContentTypeId(int contentTypeId) - { - _contentTypeId = contentTypeId; - return this; - } - - public ContentNodeKitBuilder WithDraftData(ContentData draftData) - { - _draftData = draftData; - return this; - } - - public ContentNodeKitBuilder WithPublishedData(ContentData publishedData) - { - _publishedData = publishedData; - return this; - } - - public override ContentNodeKit Build() - { - var data = new ContentNodeKit(_contentNode, _contentTypeId, _draftData, _publishedData); - return data; - } - - /// - /// Creates a ContentNodeKit - /// - /// - /// - /// - /// - /// - /// Optional. Will get calculated based on the path value if not specified. - /// - /// - /// Optional. Will get calculated based on the path value if not specified. - /// - /// - /// - /// - /// - /// - /// - public static ContentNodeKit CreateWithContent( - int contentTypeId, - int id, - string path, - int? sortOrder = null, - int? level = null, - int? parentContentId = null, - int creatorId = -1, - Guid? uid = null, - DateTime? createDate = null, - ContentData draftData = null, - ContentData publishedData = null) - { - var pathParts = path.Split(','); - if (pathParts.Length >= 2) - { - parentContentId ??= int.Parse(pathParts[^2]); - } - - return new ContentNodeKitBuilder() - .WithContentTypeId(contentTypeId) - .WithContentNode(id, uid ?? Guid.NewGuid(), level ?? pathParts.Length - 1, path, sortOrder ?? 0, parentContentId.Value, createDate ?? DateTime.Now, creatorId) - .WithDraftData(draftData) - .WithPublishedData(publishedData) - .Build(); - } -} +// using System; +// using Umbraco.Cms.Infrastructure.HybridCache; +// +// namespace Umbraco.Cms.Tests.Common.Builders; +// +// FIXME: Reintroduce if relevant +// internal class ContentNodeKitBuilder : BuilderBase +// { +// private ContentNode _contentNode; +// private int _contentTypeId; +// private ContentData _draftData; +// private ContentData _publishedData; +// +// public ContentNodeKitBuilder WithContentNode(ContentNode contentNode) +// { +// _contentNode = contentNode; +// return this; +// } +// +// public ContentNodeKitBuilder WithContentNode(int id, Guid uid, int level, string path, int sortOrder, int parentContentId, DateTime createDate, int creatorId) +// { +// _contentNode = new ContentNode(id, uid, level, path, sortOrder, parentContentId, createDate, creatorId); +// return this; +// } +// +// public ContentNodeKitBuilder WithContentTypeId(int contentTypeId) +// { +// _contentTypeId = contentTypeId; +// return this; +// } +// +// public ContentNodeKitBuilder WithDraftData(ContentData draftData) +// { +// _draftData = draftData; +// return this; +// } +// +// public ContentNodeKitBuilder WithPublishedData(ContentData publishedData) +// { +// _publishedData = publishedData; +// return this; +// } +// +// public override ContentNodeKit Build() +// { +// var data = new ContentNodeKit(_contentNode, _contentTypeId, _draftData, _publishedData); +// return data; +// } +// +// /// +// /// Creates a ContentNodeKit +// /// +// /// +// /// +// /// +// /// +// /// +// /// Optional. Will get calculated based on the path value if not specified. +// /// +// /// +// /// Optional. Will get calculated based on the path value if not specified. +// /// +// /// +// /// +// /// +// /// +// /// +// /// +// public static ContentNodeKit CreateWithContent( +// int contentTypeId, +// int id, +// string path, +// int? sortOrder = null, +// int? level = null, +// int? parentContentId = null, +// int creatorId = -1, +// Guid? uid = null, +// DateTime? createDate = null, +// ContentData draftData = null, +// ContentData publishedData = null) +// { +// var pathParts = path.Split(','); +// if (pathParts.Length >= 2) +// { +// parentContentId ??= int.Parse(pathParts[^2]); +// } +// +// return new ContentNodeKitBuilder() +// .WithContentTypeId(contentTypeId) +// .WithContentNode(id, uid ?? Guid.NewGuid(), level ?? pathParts.Length - 1, path, sortOrder ?? 0, parentContentId.Value, createDate ?? DateTime.Now, creatorId) +// .WithDraftData(draftData) +// .WithPublishedData(publishedData) +// .Build(); +// } +// } diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyDataBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyDataBuilder.cs index 6503667df6..c4888e2cbd 100644 --- a/tests/Umbraco.Tests.Common/Builders/PropertyDataBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/PropertyDataBuilder.cs @@ -1,29 +1,30 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -namespace Umbraco.Cms.Tests.Common.Builders; - -public class PropertyDataBuilder : BuilderBase> -{ - private readonly Dictionary> _properties = new(); - - public PropertyDataBuilder WithPropertyData(string alias, PropertyData propertyData) - { - if (!_properties.TryGetValue(alias, out var propertyDataCollection)) - { - propertyDataCollection = new List(); - _properties[alias] = propertyDataCollection; - } - - propertyDataCollection.Add(propertyData); - - return this; - } - - public PropertyDataBuilder WithPropertyData(string alias, object value, string? culture = null, string? segment = null) - => WithPropertyData(alias, new PropertyData { Culture = culture ?? string.Empty, Segment = segment ?? string.Empty, Value = value }); - - public override Dictionary Build() - => _properties.ToDictionary(x => x.Key, x => x.Value.ToArray()); -} +// using System.Collections.Generic; +// using System.Linq; +// using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +// +// namespace Umbraco.Cms.Tests.Common.Builders; +// +// FIXME: Reintroduce if relevant +// public class PropertyDataBuilder : BuilderBase> +// { +// private readonly Dictionary> _properties = new(); +// +// public PropertyDataBuilder WithPropertyData(string alias, PropertyData propertyData) +// { +// if (!_properties.TryGetValue(alias, out var propertyDataCollection)) +// { +// propertyDataCollection = new List(); +// _properties[alias] = propertyDataCollection; +// } +// +// propertyDataCollection.Add(propertyData); +// +// return this; +// } +// +// public PropertyDataBuilder WithPropertyData(string alias, object value, string? culture = null, string? segment = null) +// => WithPropertyData(alias, new PropertyData { Culture = culture ?? string.Empty, Segment = segment ?? string.Empty, Value = value }); +// +// public override Dictionary Build() +// => _properties.ToDictionary(x => x.Key, x => x.Value.ToArray()); +// } diff --git a/tests/Umbraco.Tests.Common/Published/PublishedContentXmlAdapter.cs b/tests/Umbraco.Tests.Common/Published/PublishedContentXmlAdapter.cs index d7eb030140..880faa00f7 100644 --- a/tests/Umbraco.Tests.Common/Published/PublishedContentXmlAdapter.cs +++ b/tests/Umbraco.Tests.Common/Published/PublishedContentXmlAdapter.cs @@ -1,148 +1,149 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; -using System.Xml.XPath; -using Moq; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; -using Umbraco.Cms.Infrastructure.Serialization; -using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Builders.Extensions; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.Common.Published; - -/// -/// Converts legacy Umbraco XML structures to NuCache collections -/// to populate a test implementation of -/// -/// -/// This does not support variant data because the XML structure doesn't support variant data. -/// -public static class PublishedContentXmlAdapter -{ - /// - /// Generate a collection of based on legacy umbraco XML - /// - /// The legacy umbraco XML - /// - /// Dynamically generates a list of s based on the XML data - /// Dynamically generates a list of for tests - /// - public static IEnumerable GetContentNodeKits( - string xml, - IShortStringHelper shortStringHelper, - out ContentType[] contentTypes, - out DataType[] dataTypes) - { - // use the label data type for all data for these tests except in the case - // where a property is named 'content', in which case use the RTE. - var serializer = new SystemTextConfigurationEditorJsonSerializer(); - var labelDataType = - new DataType(new VoidEditor("Label", Mock.Of()), serializer) { Id = 3 }; - var rteDataType = new DataType(new VoidEditor("RTE", Mock.Of()), serializer) { Id = 4 }; - dataTypes = new[] { labelDataType, rteDataType }; - - var kitsAndXml = new List<(ContentNodeKit kit, XElement node)>(); - - var xDoc = XDocument.Parse(xml); - var nodes = xDoc.XPathSelectElements("//*[@isDoc]"); - foreach (var node in nodes) - { - var id = node.AttributeValue("id"); - var key = node.AttributeValue("key") ?? id.ToGuid(); - - var propertyElements = node.Elements().Where(x => x.Attribute("id") == null); - var properties = new Dictionary(); - foreach (var propertyElement in propertyElements) - { - properties[propertyElement.Name.LocalName] = new[] - { - // TODO: builder? - new PropertyData {Culture = string.Empty, Segment = string.Empty, Value = propertyElement.Value} - }; - } - - var contentData = new ContentDataBuilder() - .WithName(node.AttributeValue("nodeName")) - .WithProperties(properties) - .WithPublished(true) - .WithTemplateId(node.AttributeValue("template")) - .WithUrlSegment(node.AttributeValue("urlName")) - .WithVersionDate(node.AttributeValue("updateDate")) - .WithWriterId(node.AttributeValue("writerID")) - .Build(); - - var kit = ContentNodeKitBuilder.CreateWithContent( - node.AttributeValue("nodeType"), - id, - node.AttributeValue("path"), - node.AttributeValue("sortOrder"), - node.AttributeValue("level"), - node.AttributeValue("parentID"), - node.AttributeValue("creatorID"), - key, - node.AttributeValue("createDate"), - contentData, - contentData); - - kitsAndXml.Add((kit, node)); - } - - // put together the unique content types - var contentTypesIdToType = new Dictionary(); - foreach ((var kit, var node) in kitsAndXml) - { - if (!contentTypesIdToType.TryGetValue(kit.ContentTypeId, out var contentType)) - { - contentType = new ContentType(shortStringHelper, -1) - { - Id = kit.ContentTypeId, - Alias = node.Name.LocalName - }; - SetContentTypeProperties(shortStringHelper, labelDataType, rteDataType, kit, contentType); - contentTypesIdToType[kit.ContentTypeId] = contentType; - } - else - { - // we've already created it but might need to add properties - SetContentTypeProperties(shortStringHelper, labelDataType, rteDataType, kit, contentType); - } - } - - contentTypes = contentTypesIdToType.Values.ToArray(); - - return kitsAndXml.Select(x => x.kit); - } - - private static void SetContentTypeProperties( - IShortStringHelper shortStringHelper, - DataType labelDataType, - DataType rteDataType, - ContentNodeKit kit, - ContentType contentType) - { - foreach (var property in kit.DraftData.Properties) - { - var propertyType = new PropertyType(shortStringHelper, labelDataType, property.Key); - - if (!contentType.PropertyTypeExists(propertyType.Alias)) - { - if (propertyType.Alias == "content") - { - propertyType.DataTypeId = rteDataType.Id; - } - - contentType.AddPropertyType(propertyType); - } - } - } -} +// // Copyright (c) Umbraco. +// // See LICENSE for more details. +// +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using System.Xml.Linq; +// using System.Xml.XPath; +// using Moq; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.PropertyEditors; +// using Umbraco.Cms.Core.Strings; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +// using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; +// using Umbraco.Cms.Infrastructure.Serialization; +// using Umbraco.Cms.Tests.Common.Builders; +// using Umbraco.Cms.Tests.Common.Builders.Extensions; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.Common.Published; +// FIXME: Reintroduce if relevant +// +// /// +// /// Converts legacy Umbraco XML structures to NuCache collections +// /// to populate a test implementation of +// /// +// /// +// /// This does not support variant data because the XML structure doesn't support variant data. +// /// +// public static class PublishedContentXmlAdapter +// { +// /// +// /// Generate a collection of based on legacy umbraco XML +// /// +// /// The legacy umbraco XML +// /// +// /// Dynamically generates a list of s based on the XML data +// /// Dynamically generates a list of for tests +// /// +// public static IEnumerable GetContentNodeKits( +// string xml, +// IShortStringHelper shortStringHelper, +// out ContentType[] contentTypes, +// out DataType[] dataTypes) +// { +// // use the label data type for all data for these tests except in the case +// // where a property is named 'content', in which case use the RTE. +// var serializer = new SystemTextConfigurationEditorJsonSerializer(); +// var labelDataType = +// new DataType(new VoidEditor("Label", Mock.Of()), serializer) { Id = 3 }; +// var rteDataType = new DataType(new VoidEditor("RTE", Mock.Of()), serializer) { Id = 4 }; +// dataTypes = new[] { labelDataType, rteDataType }; +// +// var kitsAndXml = new List<(ContentNodeKit kit, XElement node)>(); +// +// var xDoc = XDocument.Parse(xml); +// var nodes = xDoc.XPathSelectElements("//*[@isDoc]"); +// foreach (var node in nodes) +// { +// var id = node.AttributeValue("id"); +// var key = node.AttributeValue("key") ?? id.ToGuid(); +// +// var propertyElements = node.Elements().Where(x => x.Attribute("id") == null); +// var properties = new Dictionary(); +// foreach (var propertyElement in propertyElements) +// { +// properties[propertyElement.Name.LocalName] = new[] +// { +// // TODO: builder? +// new PropertyData {Culture = string.Empty, Segment = string.Empty, Value = propertyElement.Value} +// }; +// } +// +// var contentData = new ContentDataBuilder() +// .WithName(node.AttributeValue("nodeName")) +// .WithProperties(properties) +// .WithPublished(true) +// .WithTemplateId(node.AttributeValue("template")) +// .WithUrlSegment(node.AttributeValue("urlName")) +// .WithVersionDate(node.AttributeValue("updateDate")) +// .WithWriterId(node.AttributeValue("writerID")) +// .Build(); +// +// var kit = ContentNodeKitBuilder.CreateWithContent( +// node.AttributeValue("nodeType"), +// id, +// node.AttributeValue("path"), +// node.AttributeValue("sortOrder"), +// node.AttributeValue("level"), +// node.AttributeValue("parentID"), +// node.AttributeValue("creatorID"), +// key, +// node.AttributeValue("createDate"), +// contentData, +// contentData); +// +// kitsAndXml.Add((kit, node)); +// } +// +// // put together the unique content types +// var contentTypesIdToType = new Dictionary(); +// foreach ((var kit, var node) in kitsAndXml) +// { +// if (!contentTypesIdToType.TryGetValue(kit.ContentTypeId, out var contentType)) +// { +// contentType = new ContentType(shortStringHelper, -1) +// { +// Id = kit.ContentTypeId, +// Alias = node.Name.LocalName +// }; +// SetContentTypeProperties(shortStringHelper, labelDataType, rteDataType, kit, contentType); +// contentTypesIdToType[kit.ContentTypeId] = contentType; +// } +// else +// { +// // we've already created it but might need to add properties +// SetContentTypeProperties(shortStringHelper, labelDataType, rteDataType, kit, contentType); +// } +// } +// +// contentTypes = contentTypesIdToType.Values.ToArray(); +// +// return kitsAndXml.Select(x => x.kit); +// } +// +// private static void SetContentTypeProperties( +// IShortStringHelper shortStringHelper, +// DataType labelDataType, +// DataType rteDataType, +// ContentNodeKit kit, +// ContentType contentType) +// { +// foreach (var property in kit.DraftData.Properties) +// { +// var propertyType = new PropertyType(shortStringHelper, labelDataType, property.Key); +// +// if (!contentType.PropertyTypeExists(propertyType.Alias)) +// { +// if (propertyType.Alias == "content") +// { +// propertyType.DataTypeId = rteDataType.Id; +// } +// +// contentType.AddPropertyType(propertyType); +// } +// } +// } +// } diff --git a/tests/Umbraco.Tests.Common/TestPublishedSnapshotAccessor.cs b/tests/Umbraco.Tests.Common/TestPublishedSnapshotAccessor.cs deleted file mode 100644 index 54e49d0283..0000000000 --- a/tests/Umbraco.Tests.Common/TestPublishedSnapshotAccessor.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using Umbraco.Cms.Core.PublishedCache; - -namespace Umbraco.Cms.Tests.Common; - -public class TestPublishedSnapshotAccessor : IPublishedSnapshotAccessor -{ - private IPublishedSnapshot _snapshot; - - public bool TryGetPublishedSnapshot(out IPublishedSnapshot publishedSnapshot) - { - publishedSnapshot = _snapshot; - return _snapshot != null; - } - - public void SetCurrent(IPublishedSnapshot snapshot) => _snapshot = snapshot; -} diff --git a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs index 41a0e4f1a7..732fc0a385 100644 --- a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs @@ -20,7 +20,6 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Infrastructure.HostedServices; -using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Persistence.EFCore.Locking; using Umbraco.Cms.Persistence.EFCore.Scoping; using Umbraco.Cms.Tests.Common.TestHelpers.Stubs; @@ -46,8 +45,6 @@ public static class UmbracoBuilderExtensions builder.Services.AddUnique(testHelper.MainDom); builder.Services.AddUnique(); - // we don't want persisted nucache files in tests - builder.Services.AddTransient(factory => new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }); #if IS_WINDOWS // ensure all lucene indexes are using RAM directory (no file system) diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index b4437397d3..494d51ee58 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -256,7 +256,6 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest .AddConfiguration() .AddUmbracoCore() .AddWebComponents() - .AddNuCache() .AddUmbracoHybridCache() .AddBackOfficeCore() .AddBackOfficeAuthentication() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs index 7c4b62f2af..d74439634a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs @@ -12,7 +12,6 @@ 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(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/CacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/CacheTests.cs index 7c13e5780d..3bad270d45 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/CacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/CacheTests.cs @@ -1,76 +1,77 @@ -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi; - -[TestFixture] -public class CacheTests -{ - [TestCase(PropertyCacheLevel.Snapshot, false, 1)] - [TestCase(PropertyCacheLevel.Snapshot, true, 1)] - [TestCase(PropertyCacheLevel.Elements, false, 1)] - [TestCase(PropertyCacheLevel.Elements, true, 1)] - [TestCase(PropertyCacheLevel.Element, false, 1)] - [TestCase(PropertyCacheLevel.Element, true, 1)] - [TestCase(PropertyCacheLevel.None, false, 4)] - [TestCase(PropertyCacheLevel.None, true, 4)] - public void PublishedElementProperty_CachesDeliveryApiValueConversion(PropertyCacheLevel cacheLevel, bool expanding, int expectedConverterHits) - { - var contentType = new Mock(); - contentType.SetupGet(c => c.PropertyTypes).Returns(Array.Empty()); - - var contentNode = new ContentNode(123, Guid.NewGuid(), contentType.Object, 1, string.Empty, 1, 1, DateTime.Now, 1); - var contentData = new ContentData("bla", "bla", 1, DateTime.Now, 1, 1, true, new Dictionary(), null); - - var elementCache = new FastDictionaryAppCache(); - var snapshotCache = new FastDictionaryAppCache(); - var publishedSnapshotMock = new Mock(); - publishedSnapshotMock.SetupGet(p => p.ElementsCache).Returns(elementCache); - publishedSnapshotMock.SetupGet(p => p.SnapshotCache).Returns(snapshotCache); - - var publishedSnapshot = publishedSnapshotMock.Object; - var publishedSnapshotAccessor = new Mock(); - publishedSnapshotAccessor.Setup(p => p.TryGetPublishedSnapshot(out publishedSnapshot)).Returns(true); - - var content = new PublishedContent( - contentNode, - contentData, - publishedSnapshotAccessor.Object, - Mock.Of(), - Mock.Of()); - - var propertyType = new Mock(); - var invocationCount = 0; - propertyType.SetupGet(p => p.CacheLevel).Returns(cacheLevel); - propertyType.SetupGet(p => p.DeliveryApiCacheLevel).Returns(cacheLevel); - propertyType.SetupGet(p => p.DeliveryApiCacheLevelForExpansion).Returns(cacheLevel); - propertyType - .Setup(p => p.ConvertInterToDeliveryApiObject(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(() => $"Delivery API value: {++invocationCount}"); - - var prop1 = new Property(propertyType.Object, content, publishedSnapshotAccessor.Object); - var results = new List(); - results.Add(prop1.GetDeliveryApiValue(expanding)!.ToString()); - results.Add(prop1.GetDeliveryApiValue(expanding)!.ToString()); - results.Add(prop1.GetDeliveryApiValue(expanding)!.ToString()); - results.Add(prop1.GetDeliveryApiValue(expanding)!.ToString()); - - Assert.AreEqual("Delivery API value: 1", results.First()); - Assert.AreEqual(expectedConverterHits, results.Distinct().Count()); - - propertyType.Verify( - p => p.ConvertInterToDeliveryApiObject( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Exactly(expectedConverterHits)); - } -} +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Cache; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Core.PropertyEditors; +// using Umbraco.Cms.Core.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +// +// namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class CacheTests +// { +// [TestCase(PropertyCacheLevel.Snapshot, false, 1)] +// [TestCase(PropertyCacheLevel.Snapshot, true, 1)] +// [TestCase(PropertyCacheLevel.Elements, false, 1)] +// [TestCase(PropertyCacheLevel.Elements, true, 1)] +// [TestCase(PropertyCacheLevel.Element, false, 1)] +// [TestCase(PropertyCacheLevel.Element, true, 1)] +// [TestCase(PropertyCacheLevel.None, false, 4)] +// [TestCase(PropertyCacheLevel.None, true, 4)] +// public void PublishedElementProperty_CachesDeliveryApiValueConversion(PropertyCacheLevel cacheLevel, bool expanding, int expectedConverterHits) +// { +// var contentType = new Mock(); +// contentType.SetupGet(c => c.PropertyTypes).Returns(Array.Empty()); +// +// var contentNode = new ContentNode(123, Guid.NewGuid(), contentType.Object, 1, string.Empty, 1, 1, DateTime.Now, 1); +// var contentData = new ContentData("bla", "bla", 1, DateTime.Now, 1, 1, true, new Dictionary(), null); +// +// var elementCache = new FastDictionaryAppCache(); +// var snapshotCache = new FastDictionaryAppCache(); +// var publishedSnapshotMock = new Mock(); +// publishedSnapshotMock.SetupGet(p => p.ElementsCache).Returns(elementCache); +// publishedSnapshotMock.SetupGet(p => p.SnapshotCache).Returns(snapshotCache); +// +// var publishedSnapshot = publishedSnapshotMock.Object; +// var publishedSnapshotAccessor = new Mock(); +// publishedSnapshotAccessor.Setup(p => p.TryGetPublishedSnapshot(out publishedSnapshot)).Returns(true); +// +// var content = new PublishedContent( +// contentNode, +// contentData, +// publishedSnapshotAccessor.Object, +// Mock.Of(), +// Mock.Of()); +// +// var propertyType = new Mock(); +// var invocationCount = 0; +// propertyType.SetupGet(p => p.CacheLevel).Returns(cacheLevel); +// propertyType.SetupGet(p => p.DeliveryApiCacheLevel).Returns(cacheLevel); +// propertyType.SetupGet(p => p.DeliveryApiCacheLevelForExpansion).Returns(cacheLevel); +// propertyType +// .Setup(p => p.ConvertInterToDeliveryApiObject(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) +// .Returns(() => $"Delivery API value: {++invocationCount}"); +// +// var prop1 = new Property(propertyType.Object, content, publishedSnapshotAccessor.Object); +// var results = new List(); +// results.Add(prop1.GetDeliveryApiValue(expanding)!.ToString()); +// results.Add(prop1.GetDeliveryApiValue(expanding)!.ToString()); +// results.Add(prop1.GetDeliveryApiValue(expanding)!.ToString()); +// results.Add(prop1.GetDeliveryApiValue(expanding)!.ToString()); +// +// Assert.AreEqual("Delivery API value: 1", results.First()); +// Assert.AreEqual(expectedConverterHits, results.Distinct().Count()); +// +// propertyType.Verify( +// p => p.ConvertInterToDeliveryApiObject( +// It.IsAny(), +// It.IsAny(), +// It.IsAny(), +// It.IsAny(), +// It.IsAny()), +// Times.Exactly(expectedConverterHits)); +// } +// } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs index 093e446adf..bdc5adfe80 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs @@ -77,11 +77,10 @@ public class PublishedContentQueryTests : ExamineBaseTest var contentCache = new Mock(); contentCache.Setup(x => x.GetById(It.IsAny())) .Returns((int intId) => Mock.Of(x => x.Id == intId)); - var snapshot = Mock.Of(x => x.Content == contentCache.Object); var variationContext = new VariationContext(); var variationContextAccessor = Mock.Of(x => x.VariationContext == variationContext); - return new PublishedContentQuery(snapshot, variationContextAccessor, examineManager.Object); + return new PublishedContentQuery(variationContextAccessor, examineManager.Object, contentCache.Object, Mock.Of()); } [TestCase("fr-fr", ExpectedResult = "1, 3", Description = "Search Culture: fr-fr. Must return both fr-fr and invariant results")] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs index cd138283eb..35194277c2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs @@ -35,16 +35,16 @@ public class AdvancedMigrationTests : UmbracoIntegrationTest private ILoggerFactory LoggerFactory => GetRequiredService(); private IMigrationBuilder MigrationBuilder => GetRequiredService(); private IUmbracoDatabaseFactory UmbracoDatabaseFactory => GetRequiredService(); - private IPublishedSnapshotService PublishedSnapshotService => GetRequiredService(); private IServiceScopeFactory ServiceScopeFactory => GetRequiredService(); private DistributedCache DistributedCache => GetRequiredService(); + private IDatabaseCacheRebuilder DatabaseCacheRebuilder => GetRequiredService(); private IMigrationPlanExecutor MigrationPlanExecutor => new MigrationPlanExecutor( CoreScopeProvider, ScopeAccessor, LoggerFactory, MigrationBuilder, UmbracoDatabaseFactory, - PublishedSnapshotService, + DatabaseCacheRebuilder, DistributedCache, Mock.Of(), ServiceScopeFactory); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs index 2c1f69de8a..daada604eb 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs @@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.HybridCache.Services; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -35,8 +36,6 @@ public abstract class BlockEditorElementVariationTestBase : UmbracoIntegrationTe protected PropertyEditorCollection PropertyEditorCollection => GetRequiredService(); - private IPublishedSnapshotService PublishedSnapshotService => GetRequiredService(); - private IUmbracoContextAccessor UmbracoContextAccessor => GetRequiredService(); private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService(); @@ -47,6 +46,8 @@ public abstract class BlockEditorElementVariationTestBase : UmbracoIntegrationTe private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService(); + private IDocumentCacheService DocumentCacheService => GetRequiredService(); + protected override void CustomTestSetup(IUmbracoBuilder builder) { var mockHttpContextAccessor = new Mock(); @@ -59,7 +60,6 @@ public abstract class BlockEditorElementVariationTestBase : UmbracoIntegrationTe builder.Services.AddUnique(); builder.Services.AddUnique(mockHttpContextAccessor.Object); builder.AddUmbracoHybridCache(); - builder.AddNuCache(); builder.Services.Configure(config => config.AllowEditInvariantFromNonDefault = TestsRequiringAllowEditInvariantFromNonDefault.Contains(TestContext.CurrentContext.Test.Name)); @@ -74,18 +74,19 @@ public abstract class BlockEditorElementVariationTestBase : UmbracoIntegrationTe var publishResult = ContentService.Publish(content, culturesToPublish); Assert.IsTrue(publishResult.Success); - ContentCacheRefresher.JsonPayload[] payloads = - [ - new ContentCacheRefresher.JsonPayload - { - ChangeTypes = TreeChangeTypes.RefreshNode, - Key = content.Key, - Id = content.Id, - Blueprint = false - } - ]; + // ContentCacheRefresher.JsonPayload[] payloads = + // [ + // new ContentCacheRefresher.JsonPayload + // { + // ChangeTypes = TreeChangeTypes.RefreshNode, + // Key = content.Key, + // Id = content.Id, + // Blueprint = false + // } + // ]; - PublishedSnapshotService.Notify(payloads, out _, out _); + + DocumentCacheService.RefreshContentAsync(content); } protected IContentType CreateElementType(ContentVariation variation, string alias = "myElementType") @@ -163,10 +164,10 @@ public abstract class BlockEditorElementVariationTestBase : UmbracoIntegrationTe protected void RefreshContentTypeCache(params IContentType[] contentTypes) { - ContentTypeCacheRefresher.JsonPayload[] payloads = contentTypes - .Select(contentType => new ContentTypeCacheRefresher.JsonPayload(nameof(IContentType), contentType.Id, ContentTypeChangeTypes.RefreshMain)) - .ToArray(); + // ContentTypeCacheRefresher.JsonPayload[] payloads = contentTypes + // .Select(contentType => new ContentTypeCacheRefresher.JsonPayload(nameof(IContentType), contentType.Id, ContentTypeChangeTypes.RefreshMain)) + // .ToArray(); - PublishedSnapshotService.Notify(payloads); + DocumentCacheService.Rebuild(contentTypes.Select(x => x.Id).ToArray()); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedNuCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedNuCacheTests.cs index e727060d98..7c9d234c16 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedNuCacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedNuCacheTests.cs @@ -37,7 +37,6 @@ public class ScopedNuCacheTests : UmbracoIntegrationTest builder.AddNotificationHandler(); builder.Services.AddUnique(); builder.Services.AddUnique(MockHttpContextAccessor.Object); - builder.AddNuCache(); } public class NotificationHandler : INotificationHandler diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs index 8e9206b1bf..833746d5f8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs @@ -1,26 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Cms.Infrastructure.Sync; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; -using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping; @@ -47,7 +42,7 @@ public class ScopedRepositoryTests : UmbracoIntegrationTest protected override void CustomTestSetup(IUmbracoBuilder builder) { - builder.AddNuCache(); + builder.AddUmbracoHybridCache(); builder.Services.AddUnique(); builder .AddNotificationHandler() @@ -58,7 +53,7 @@ public class ScopedRepositoryTests : UmbracoIntegrationTest .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler(); - builder.AddNotificationHandler(); + // builder.AddNotificationHandler(); } [TestCase(true)] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs index dc7b78b6fd..c22e9fbb29 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs @@ -24,12 +24,6 @@ public class CacheInstructionServiceTests : UmbracoIntegrationTest private CacheRefresherCollection CacheRefreshers => GetRequiredService(); private IServerRoleAccessor ServerRoleAccessor => GetRequiredService(); - protected override void CustomTestSetup(IUmbracoBuilder builder) - { - base.CustomTestSetup(builder); - builder.AddNuCache(); - } - [Test] public void Confirms_Cold_Boot_Required_When_Instructions_Exist_And_None_Have_Been_Synced() { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs index 0c843cd5d6..2e329e664b 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs @@ -164,7 +164,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services } protected override void CustomTestSetup(IUmbracoBuilder builder) { - builder.AddNuCache(); + builder.AddUmbracoHybridCache(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs index e4381ff3d9..25f62b475a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs @@ -36,7 +36,7 @@ public class ContentTypeServiceVariantsTests : UmbracoIntegrationTest protected override void CustomTestSetup(IUmbracoBuilder builder) { - builder.AddNuCache(); + builder.AddUmbracoHybridCache(); builder.Services.AddUnique(); builder.Services.PostConfigure(options => { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs index 64e741752d..19a2241e57 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs @@ -1,24 +1,17 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.IO; -using System.Linq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; -using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Persistence.Querying; -using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.HybridCache.Factories; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; -using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; @@ -32,6 +25,8 @@ public class MemberServiceTests : UmbracoIntegrationTest private IMemberService MemberService => GetRequiredService(); + private IPublishedContentFactory PublishedContentFactory => GetRequiredService(); + [Test] public void Can_Update_Member_Property_Values() { @@ -213,15 +208,7 @@ public class MemberServiceTests : UmbracoIntegrationTest member = MemberService.GetById(member.Id); Assert.AreEqual("xemail", member.Email); - var contentTypeFactory = new PublishedContentTypeFactory(new NoopPublishedModelFactory(), - new PropertyValueConverterCollection(() => Enumerable.Empty()), - GetRequiredService()); - var pmemberType = new PublishedContentType(memberType, contentTypeFactory); - - var publishedSnapshotAccessor = new TestPublishedSnapshotAccessor(); - var variationContextAccessor = new TestVariationContextAccessor(); - var pmember = PublishedMember.Create(member, pmemberType, false, publishedSnapshotAccessor, - variationContextAccessor, GetRequiredService()); + var pmember = PublishedContentFactory.ToPublishedMember(member); // contains the umbracoMember... properties created when installing, on the member type // contains the other properties, that PublishedContentType adds (BuiltinMemberProperties) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/NuCacheRebuildTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/NuCacheRebuildTests.cs index 6ad3b202ae..5d1c434a45 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/NuCacheRebuildTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/NuCacheRebuildTests.cs @@ -1,88 +1,89 @@ -using NUnit.Framework; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Testing; -using Umbraco.Cms.Tests.Integration.Testing; - -namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; - -[TestFixture] -[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, - WithApplication = true)] -public class NuCacheRebuildTests : UmbracoIntegrationTest -{ - private IFileService FileService => GetRequiredService(); - - private IContentService ContentService => GetRequiredService(); - - private IContentTypeService ContentTypeService => GetRequiredService(); - - private IPublishedSnapshotService PublishedSnapshotService => GetRequiredService(); - - [Test] - public void UnpublishedNameChanges() - { - var urlSegmentProvider = new DefaultUrlSegmentProvider(ShortStringHelper); - - var template = TemplateBuilder.CreateTextPageTemplate(); - FileService.SaveTemplate(template); - - var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id); - ContentTypeService.Save(contentType); - - var content = ContentBuilder.CreateTextpageContent(contentType, "hello", Constants.System.Root); - - ContentService.Save(content); - ContentService.Publish(content, Array.Empty()); - var cachedContent = ContentService.GetById(content.Id); - var segment = urlSegmentProvider.GetUrlSegment(cachedContent); - - // Does a new node work? - - Assert.AreEqual("hello", segment); - - content.Name = "goodbye"; - cachedContent = ContentService.GetById(content.Id); - segment = urlSegmentProvider.GetUrlSegment(cachedContent); - - // We didn't save anything, so all should still be the same - - Assert.AreEqual("hello", segment); - - ContentService.Save(content); - cachedContent = ContentService.GetById(content.Id); - segment = urlSegmentProvider.GetUrlSegment(cachedContent); - - // At this point we have saved the new name, but not published. The url should still be the previous name - - Assert.AreEqual("hello", segment); - - PublishedSnapshotService.RebuildAll(); - - cachedContent = ContentService.GetById(content.Id); - segment = urlSegmentProvider.GetUrlSegment(cachedContent); - - // After a rebuild, the unpublished name should still not be the url. - // This was previously incorrect, per #11074 - - Assert.AreEqual("hello", segment); - - ContentService.Save(content); - ContentService.Publish(content, Array.Empty()); - cachedContent = ContentService.GetById(content.Id); - segment = urlSegmentProvider.GetUrlSegment(cachedContent); - - // The page has now been published, so we should see the new url segment - Assert.AreEqual("goodbye", segment); - - PublishedSnapshotService.RebuildAll(); - cachedContent = ContentService.GetById(content.Id); - segment = urlSegmentProvider.GetUrlSegment(cachedContent); - - // Just double checking that things remain after a rebuild - Assert.AreEqual("goodbye", segment); - } -} +// using NUnit.Framework; +// using Umbraco.Cms.Core; +// using Umbraco.Cms.Core.PublishedCache; +// using Umbraco.Cms.Core.Services; +// using Umbraco.Cms.Core.Strings; +// using Umbraco.Cms.Tests.Common.Builders; +// using Umbraco.Cms.Tests.Common.Testing; +// using Umbraco.Cms.Tests.Integration.Testing; +// +// namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; +// +// FIXME: Reintroduce if needed +// [TestFixture] +// [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, +// WithApplication = true)] +// public class NuCacheRebuildTests : UmbracoIntegrationTest +// { +// private IFileService FileService => GetRequiredService(); +// +// private IContentService ContentService => GetRequiredService(); +// +// private IContentTypeService ContentTypeService => GetRequiredService(); +// +// private IPublishedSnapshotService PublishedSnapshotService => GetRequiredService(); +// +// [Test] +// public void UnpublishedNameChanges() +// { +// var urlSegmentProvider = new DefaultUrlSegmentProvider(ShortStringHelper); +// +// var template = TemplateBuilder.CreateTextPageTemplate(); +// FileService.SaveTemplate(template); +// +// var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id); +// ContentTypeService.Save(contentType); +// +// var content = ContentBuilder.CreateTextpageContent(contentType, "hello", Constants.System.Root); +// +// ContentService.Save(content); +// ContentService.Publish(content, Array.Empty()); +// var cachedContent = ContentService.GetById(content.Id); +// var segment = urlSegmentProvider.GetUrlSegment(cachedContent); +// +// // Does a new node work? +// +// Assert.AreEqual("hello", segment); +// +// content.Name = "goodbye"; +// cachedContent = ContentService.GetById(content.Id); +// segment = urlSegmentProvider.GetUrlSegment(cachedContent); +// +// // We didn't save anything, so all should still be the same +// +// Assert.AreEqual("hello", segment); +// +// ContentService.Save(content); +// cachedContent = ContentService.GetById(content.Id); +// segment = urlSegmentProvider.GetUrlSegment(cachedContent); +// +// // At this point we have saved the new name, but not published. The url should still be the previous name +// +// Assert.AreEqual("hello", segment); +// +// PublishedSnapshotService.RebuildAll(); +// +// cachedContent = ContentService.GetById(content.Id); +// segment = urlSegmentProvider.GetUrlSegment(cachedContent); +// +// // After a rebuild, the unpublished name should still not be the url. +// // This was previously incorrect, per #11074 +// +// Assert.AreEqual("hello", segment); +// +// ContentService.Save(content); +// ContentService.Publish(content, Array.Empty()); +// cachedContent = ContentService.GetById(content.Id); +// segment = urlSegmentProvider.GetUrlSegment(cachedContent); +// +// // The page has now been published, so we should see the new url segment +// Assert.AreEqual("goodbye", segment); +// +// PublishedSnapshotService.RebuildAll(); +// cachedContent = ContentService.GetById(content.Id); +// segment = urlSegmentProvider.GetUrlSegment(cachedContent); +// +// // Just double checking that things remain after a rebuild +// Assert.AreEqual("goodbye", segment); +// } +// } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackRelationsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackRelationsTests.cs index cbcc0e4a3d..f992aa57b1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackRelationsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackRelationsTests.cs @@ -25,11 +25,11 @@ public class TrackRelationsTests : UmbracoIntegrationTestWithContent private IRelationService RelationService => GetRequiredService(); - protected override void CustomTestSetup(IUmbracoBuilder builder) - { - base.CustomTestSetup(builder); - builder.AddNuCache(); - } + // protected override void CustomTestSetup(IUmbracoBuilder builder) + // { + // base.CustomTestSetup(builder); + // builder.AddNuCache(); + // } [Test] [LongRunning] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs index 57503c71f2..8f25c27e58 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs @@ -9,7 +9,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] -[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithContentEditing { protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs index d6402b2603..22740f9eb5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs @@ -21,7 +21,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] -[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent { private IPublishedContentCache _mockedCache; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs index 68eddf35df..f9ddfb05ba 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs @@ -17,7 +17,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] -[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest { protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheScopeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheScopeTests.cs index 8f2ad58ad6..227ee4f58f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheScopeTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheScopeTests.cs @@ -10,7 +10,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] -[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] public class DocumentHybridCacheScopeTests : UmbracoIntegrationTestWithContentEditing { protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs index d7d04b64fb..06828253b8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs @@ -1,44 +1,43 @@ -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); - } -} +// 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; +// FIXME +// namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; +// +// [TestFixture] +// [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +// 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 bf2bfaddb4..69efb9a3e5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs @@ -1,495 +1,494 @@ -using NUnit.Framework; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models.ContentEditing; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Tests.Common.Testing; -using Umbraco.Cms.Tests.Integration.Testing; - -namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; - -[TestFixture] -[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] -[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] -public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing -{ - protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); - - private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); - - private IContentEditingService ContentEditingService => GetRequiredService(); - - private IContentPublishingService ContentPublishingService => GetRequiredService(); - - private const string NewName = "New Name"; - private const string NewTitle = "New Title"; - - [Test] - public async Task Can_Get_Draft_Content_By_Id() - { - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); - - // Assert - AssertTextPage(textPage); - } - - [Test] - public async Task Can_Get_Draft_Content_By_Key() - { - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); - - // Assert - AssertTextPage(textPage); - } - - [Test] - public async Task Can_Get_Published_Content_By_Id() - { - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId); - - // Assert - AssertPublishedTextPage(textPage); - } - - [Test] - public async Task Can_Get_Published_Content_By_Key() - { - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value); - - // Assert - AssertPublishedTextPage(textPage); - } - - [Test] - public async Task Can_Get_Draft_Of_Published_Content_By_Id() - { - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true); - - // Assert - AssertPublishedTextPage(textPage); - Assert.IsFalse(textPage.IsPublished()); - } - - [Test] - public async Task Can_Get_Draft_Of_Published_Content_By_Key() - { - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); - - // Assert - AssertPublishedTextPage(textPage); - Assert.IsFalse(textPage.IsPublished()); - } - - [Test] - public async Task Can_Get_Updated_Draft_Content_By_Id() - { - // Arrange - Textpage.InvariantName = NewName; - ContentUpdateModel updateModel = new ContentUpdateModel - { - InvariantName = NewName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, - }; - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); - - // Act - var updatedPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); - - // Assert - Assert.AreEqual(NewName, updatedPage.Name); - } - - [Test] - public async Task Can_Get_Updated_Draft_Content_By_Key() - { - // Arrange - Textpage.InvariantName = NewName; - ContentUpdateModel updateModel = new ContentUpdateModel - { - InvariantName = NewName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, - }; - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); - - // Act - var updatedPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); - - // Assert - Assert.AreEqual(NewName, updatedPage.Name); - } - - [Test] - [TestCase(true, true)] - [TestCase(false, false)] - // BETTER NAMING, CURRENTLY THIS IS TESTING BOTH THE PUBLISHED AND THE DRAFT OF THE PUBLISHED. - public async Task Can_Get_Updated_Draft_Published_Content_By_Id(bool preview, bool result) - { - // Arrange - PublishedTextPage.InvariantName = NewName; - ContentUpdateModel updateModel = new ContentUpdateModel - { - InvariantName = NewName, - InvariantProperties = PublishedTextPage.InvariantProperties, - Variants = PublishedTextPage.Variants, - TemplateKey = PublishedTextPage.TemplateKey, - }; - await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview); - - // Assert - Assert.AreEqual(result, NewName.Equals(textPage.Name)); - } - - [Test] - [TestCase(true, true)] - [TestCase(false, false)] - // BETTER NAMING, CURRENTLY THIS IS TESTING BOTH THE PUBLISHED AND THE DRAFT OF THE PUBLISHED. - public async Task Can_Get_Updated_Draft_Published_Content_By_Key(bool preview, bool result) - { - // Arrange - PublishedTextPage.InvariantName = NewName; - ContentUpdateModel updateModel = new ContentUpdateModel - { - InvariantName = NewName, - InvariantProperties = PublishedTextPage.InvariantProperties, - Variants = PublishedTextPage.Variants, - TemplateKey = PublishedTextPage.TemplateKey, - }; - await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, preview); - - // Assert - Assert.AreEqual(result, NewName.Equals(textPage.Name)); - } - - [Test] - public async Task Can_Get_Draft_Content_Property_By_Id() - { - // Arrange - var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); - - // Assert - Assert.AreEqual(titleValue, textPage.Value("title")); - } - - [Test] - public async Task Can_Get_Draft_Content_Property_By_Key() - { - // Arrange - var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); - - // Assert - Assert.AreEqual(titleValue, textPage.Value("title")); - } - - [Test] - public async Task Can_Get_Published_Content_Property_By_Id() - { - // Arrange - var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true); - - // Assert - Assert.AreEqual(titleValue, textPage.Value("title")); - } - - [Test] - public async Task Can_Get_Published_Content_Property_By_Key() - { - // Arrange - var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); - - // Assert - Assert.AreEqual(titleValue, textPage.Value("title")); - } - - [Test] - public async Task Can_Get_Draft_Of_Published_Content_Property_By_Id() - { - // Arrange - var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true); - - // Assert - Assert.AreEqual(titleValue, textPage.Value("title")); - } - - [Test] - public async Task Can_Get_Draft_Of_Published_Content_Property_By_Key() - { - // Arrange - var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); - - // Assert - Assert.AreEqual(titleValue, textPage.Value("title")); - } - - [Test] - public async Task Can_Get_Updated_Draft_Content_Property_By_Id() - { - // Arrange - Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - ContentUpdateModel updateModel = new ContentUpdateModel - { - InvariantName = Textpage.InvariantName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, - }; - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); - - // Assert - Assert.AreEqual(NewTitle, textPage.Value("title")); - } - - [Test] - public async Task Can_Get_Updated_Draft_Content_Property_By_Key() - { - // Arrange - Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - ContentUpdateModel updateModel = new ContentUpdateModel - { - InvariantName = Textpage.InvariantName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, - }; - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); - - // Assert - Assert.AreEqual(NewTitle, textPage.Value("title")); - } - - [Test] - public async Task Can_Get_Updated_Published_Content_Property_By_Id() - { - // Arrange - PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - ContentUpdateModel updateModel = new ContentUpdateModel - { - InvariantName = PublishedTextPage.InvariantName, - InvariantProperties = PublishedTextPage.InvariantProperties, - Variants = PublishedTextPage.Variants, - TemplateKey = PublishedTextPage.TemplateKey, - }; - 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(PublishedTextPage.Key.Value, true); - - // Assert - Assert.AreEqual(NewTitle, textPage.Value("title")); - } - - [Test] - public async Task Can_Get_Updated_Published_Content_Property_By_Key() - { - // Arrange - PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - ContentUpdateModel updateModel = new ContentUpdateModel - { - InvariantName = PublishedTextPage.InvariantName, - InvariantProperties = PublishedTextPage.InvariantProperties, - Variants = PublishedTextPage.Variants, - TemplateKey = PublishedTextPage.TemplateKey, - }; - 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(PublishedTextPage.Key.Value); - - // Assert - Assert.AreEqual(NewTitle, textPage.Value("title")); - } - - [Test] - [TestCase(true, "New Title")] - [TestCase(false, "Welcome to our Home page")] - public async Task Can_Get_Updated_Draft_Of_Published_Content_Property_By_Id(bool preview, string titleName) - { - // Arrange - PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - ContentUpdateModel updateModel = new ContentUpdateModel - { - InvariantName = PublishedTextPage.InvariantName, - InvariantProperties = PublishedTextPage.InvariantProperties, - Variants = PublishedTextPage.Variants, - TemplateKey = PublishedTextPage.TemplateKey, - }; - await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview); - - // Assert - Assert.AreEqual(titleName, textPage.Value("title")); - } - - [Test] - [TestCase(true, "New Name")] - [TestCase(false, "Welcome to our Home page")] - public async Task Can_Get_Updated_Draft_Of_Published_Content_Property_By_Key(bool preview, string titleName) - { - // Arrange - PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = titleName; - ContentUpdateModel updateModel = new ContentUpdateModel - { - InvariantName = PublishedTextPage.InvariantName, - InvariantProperties = PublishedTextPage.InvariantProperties, - Variants = PublishedTextPage.Variants, - TemplateKey = PublishedTextPage.TemplateKey, - }; - await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); - - // Assert - Assert.AreEqual(titleName, textPage.Value("title")); - } - - [Test] - public async Task Can_Not_Get_Deleted_Content_By_Id() - { - // Arrange - var content = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true); - Assert.IsNotNull(content); - await ContentEditingService.DeleteAsync(Subpage1.Key.Value, Constants.Security.SuperUserKey); - - // Act - var textPagePublishedContent = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, false); - - var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true); - - // Assert - Assert.IsNull(textPage); - } - - [Test] - public async Task Can_Not_Get_Deleted_Content_By_Key() - { - // Arrange - 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(Subpage1.Key.Value, true); - - // Assert - Assert.IsNull(textPage); - } - - [Test] - [TestCase(true)] - [TestCase(false)] - public async Task Can_Not_Get_Deleted_Published_Content_By_Id(bool preview) - { - // Arrange - await ContentEditingService.DeleteAsync(PublishedTextPage.Key.Value, Constants.Security.SuperUserKey); - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview); - - // Assert - Assert.IsNull(textPage); - } - - [Test] - [TestCase(true)] - [TestCase(false)] - public async Task Can_Not_Get_Deleted_Published_Content_By_Key(bool preview) - { - // Arrange - await ContentEditingService.DeleteAsync(PublishedTextPage.Key.Value, Constants.Security.SuperUserKey); - - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, preview); - - // Assert - Assert.IsNull(textPage); - } - - private void AssertTextPage(IPublishedContent textPage) - { - Assert.Multiple(() => - { - Assert.IsNotNull(textPage); - Assert.AreEqual(Textpage.Key, textPage.Key); - Assert.AreEqual(Textpage.ContentTypeKey, textPage.ContentType.Key); - Assert.AreEqual(Textpage.InvariantName, textPage.Name); - }); - - AssertProperties(Textpage.InvariantProperties, textPage.Properties); - } - - private void 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) - { - AssertProperty(prop, publishedProperties.First(x => x.Alias == prop.Alias)); - } - } - - private void AssertProperty(PropertyValueModel property, IPublishedProperty publishedProperty) - { - Assert.Multiple(() => - { - Assert.AreEqual(property.Alias, publishedProperty.Alias); - Assert.AreEqual(property.Value, publishedProperty.GetSourceValue()); - }); - } -} +// using NUnit.Framework; +// using Umbraco.Cms.Core; +// using Umbraco.Cms.Core.Models.ContentEditing; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Core.PublishedCache; +// using Umbraco.Cms.Core.Services; +// using Umbraco.Cms.Tests.Common.Testing; +// using Umbraco.Cms.Tests.Integration.Testing; +// FIXME +// namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; +// +// [TestFixture] +// [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +// public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing +// { +// protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); +// +// private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); +// +// private IContentEditingService ContentEditingService => GetRequiredService(); +// +// private IContentPublishingService ContentPublishingService => GetRequiredService(); +// +// private const string NewName = "New Name"; +// private const string NewTitle = "New Title"; +// +// [Test] +// public async Task Can_Get_Draft_Content_By_Id() +// { +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); +// +// // Assert +// AssertTextPage(textPage); +// } +// +// [Test] +// public async Task Can_Get_Draft_Content_By_Key() +// { +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); +// +// // Assert +// AssertTextPage(textPage); +// } +// +// [Test] +// public async Task Can_Get_Published_Content_By_Id() +// { +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId); +// +// // Assert +// AssertPublishedTextPage(textPage); +// } +// +// [Test] +// public async Task Can_Get_Published_Content_By_Key() +// { +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value); +// +// // Assert +// AssertPublishedTextPage(textPage); +// } +// +// [Test] +// public async Task Can_Get_Draft_Of_Published_Content_By_Id() +// { +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true); +// +// // Assert +// AssertPublishedTextPage(textPage); +// Assert.IsFalse(textPage.IsPublished()); +// } +// +// [Test] +// public async Task Can_Get_Draft_Of_Published_Content_By_Key() +// { +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); +// +// // Assert +// AssertPublishedTextPage(textPage); +// Assert.IsFalse(textPage.IsPublished()); +// } +// +// [Test] +// public async Task Can_Get_Updated_Draft_Content_By_Id() +// { +// // Arrange +// Textpage.InvariantName = NewName; +// ContentUpdateModel updateModel = new ContentUpdateModel +// { +// InvariantName = NewName, +// InvariantProperties = Textpage.InvariantProperties, +// Variants = Textpage.Variants, +// TemplateKey = Textpage.TemplateKey, +// }; +// await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); +// +// // Act +// var updatedPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); +// +// // Assert +// Assert.AreEqual(NewName, updatedPage.Name); +// } +// +// [Test] +// public async Task Can_Get_Updated_Draft_Content_By_Key() +// { +// // Arrange +// Textpage.InvariantName = NewName; +// ContentUpdateModel updateModel = new ContentUpdateModel +// { +// InvariantName = NewName, +// InvariantProperties = Textpage.InvariantProperties, +// Variants = Textpage.Variants, +// TemplateKey = Textpage.TemplateKey, +// }; +// await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); +// +// // Act +// var updatedPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); +// +// // Assert +// Assert.AreEqual(NewName, updatedPage.Name); +// } +// +// [Test] +// [TestCase(true, true)] +// [TestCase(false, false)] +// // BETTER NAMING, CURRENTLY THIS IS TESTING BOTH THE PUBLISHED AND THE DRAFT OF THE PUBLISHED. +// public async Task Can_Get_Updated_Draft_Published_Content_By_Id(bool preview, bool result) +// { +// // Arrange +// PublishedTextPage.InvariantName = NewName; +// ContentUpdateModel updateModel = new ContentUpdateModel +// { +// InvariantName = NewName, +// InvariantProperties = PublishedTextPage.InvariantProperties, +// Variants = PublishedTextPage.Variants, +// TemplateKey = PublishedTextPage.TemplateKey, +// }; +// await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview); +// +// // Assert +// Assert.AreEqual(result, NewName.Equals(textPage.Name)); +// } +// +// [Test] +// [TestCase(true, true)] +// [TestCase(false, false)] +// // BETTER NAMING, CURRENTLY THIS IS TESTING BOTH THE PUBLISHED AND THE DRAFT OF THE PUBLISHED. +// public async Task Can_Get_Updated_Draft_Published_Content_By_Key(bool preview, bool result) +// { +// // Arrange +// PublishedTextPage.InvariantName = NewName; +// ContentUpdateModel updateModel = new ContentUpdateModel +// { +// InvariantName = NewName, +// InvariantProperties = PublishedTextPage.InvariantProperties, +// Variants = PublishedTextPage.Variants, +// TemplateKey = PublishedTextPage.TemplateKey, +// }; +// await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, preview); +// +// // Assert +// Assert.AreEqual(result, NewName.Equals(textPage.Name)); +// } +// +// [Test] +// public async Task Can_Get_Draft_Content_Property_By_Id() +// { +// // Arrange +// var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); +// +// // Assert +// Assert.AreEqual(titleValue, textPage.Value("title")); +// } +// +// [Test] +// public async Task Can_Get_Draft_Content_Property_By_Key() +// { +// // Arrange +// var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); +// +// // Assert +// Assert.AreEqual(titleValue, textPage.Value("title")); +// } +// +// [Test] +// public async Task Can_Get_Published_Content_Property_By_Id() +// { +// // Arrange +// var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true); +// +// // Assert +// Assert.AreEqual(titleValue, textPage.Value("title")); +// } +// +// [Test] +// public async Task Can_Get_Published_Content_Property_By_Key() +// { +// // Arrange +// var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); +// +// // Assert +// Assert.AreEqual(titleValue, textPage.Value("title")); +// } +// +// [Test] +// public async Task Can_Get_Draft_Of_Published_Content_Property_By_Id() +// { +// // Arrange +// var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true); +// +// // Assert +// Assert.AreEqual(titleValue, textPage.Value("title")); +// } +// +// [Test] +// public async Task Can_Get_Draft_Of_Published_Content_Property_By_Key() +// { +// // Arrange +// var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); +// +// // Assert +// Assert.AreEqual(titleValue, textPage.Value("title")); +// } +// +// [Test] +// public async Task Can_Get_Updated_Draft_Content_Property_By_Id() +// { +// // Arrange +// Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; +// ContentUpdateModel updateModel = new ContentUpdateModel +// { +// InvariantName = Textpage.InvariantName, +// InvariantProperties = Textpage.InvariantProperties, +// Variants = Textpage.Variants, +// TemplateKey = Textpage.TemplateKey, +// }; +// await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); +// +// // Assert +// Assert.AreEqual(NewTitle, textPage.Value("title")); +// } +// +// [Test] +// public async Task Can_Get_Updated_Draft_Content_Property_By_Key() +// { +// // Arrange +// Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; +// ContentUpdateModel updateModel = new ContentUpdateModel +// { +// InvariantName = Textpage.InvariantName, +// InvariantProperties = Textpage.InvariantProperties, +// Variants = Textpage.Variants, +// TemplateKey = Textpage.TemplateKey, +// }; +// await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); +// +// // Assert +// Assert.AreEqual(NewTitle, textPage.Value("title")); +// } +// +// [Test] +// public async Task Can_Get_Updated_Published_Content_Property_By_Id() +// { +// // Arrange +// PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; +// ContentUpdateModel updateModel = new ContentUpdateModel +// { +// InvariantName = PublishedTextPage.InvariantName, +// InvariantProperties = PublishedTextPage.InvariantProperties, +// Variants = PublishedTextPage.Variants, +// TemplateKey = PublishedTextPage.TemplateKey, +// }; +// 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(PublishedTextPage.Key.Value, true); +// +// // Assert +// Assert.AreEqual(NewTitle, textPage.Value("title")); +// } +// +// [Test] +// public async Task Can_Get_Updated_Published_Content_Property_By_Key() +// { +// // Arrange +// PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; +// ContentUpdateModel updateModel = new ContentUpdateModel +// { +// InvariantName = PublishedTextPage.InvariantName, +// InvariantProperties = PublishedTextPage.InvariantProperties, +// Variants = PublishedTextPage.Variants, +// TemplateKey = PublishedTextPage.TemplateKey, +// }; +// 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(PublishedTextPage.Key.Value); +// +// // Assert +// Assert.AreEqual(NewTitle, textPage.Value("title")); +// } +// +// [Test] +// [TestCase(true, "New Title")] +// [TestCase(false, "Welcome to our Home page")] +// public async Task Can_Get_Updated_Draft_Of_Published_Content_Property_By_Id(bool preview, string titleName) +// { +// // Arrange +// PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; +// ContentUpdateModel updateModel = new ContentUpdateModel +// { +// InvariantName = PublishedTextPage.InvariantName, +// InvariantProperties = PublishedTextPage.InvariantProperties, +// Variants = PublishedTextPage.Variants, +// TemplateKey = PublishedTextPage.TemplateKey, +// }; +// await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview); +// +// // Assert +// Assert.AreEqual(titleName, textPage.Value("title")); +// } +// +// [Test] +// [TestCase(true, "New Name")] +// [TestCase(false, "Welcome to our Home page")] +// public async Task Can_Get_Updated_Draft_Of_Published_Content_Property_By_Key(bool preview, string titleName) +// { +// // Arrange +// PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = titleName; +// ContentUpdateModel updateModel = new ContentUpdateModel +// { +// InvariantName = PublishedTextPage.InvariantName, +// InvariantProperties = PublishedTextPage.InvariantProperties, +// Variants = PublishedTextPage.Variants, +// TemplateKey = PublishedTextPage.TemplateKey, +// }; +// await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); +// +// // Assert +// Assert.AreEqual(titleName, textPage.Value("title")); +// } +// +// [Test] +// public async Task Can_Not_Get_Deleted_Content_By_Id() +// { +// // Arrange +// var content = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true); +// Assert.IsNotNull(content); +// await ContentEditingService.DeleteAsync(Subpage1.Key.Value, Constants.Security.SuperUserKey); +// +// // Act +// var textPagePublishedContent = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, false); +// +// var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true); +// +// // Assert +// Assert.IsNull(textPage); +// } +// +// [Test] +// public async Task Can_Not_Get_Deleted_Content_By_Key() +// { +// // Arrange +// 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(Subpage1.Key.Value, true); +// +// // Assert +// Assert.IsNull(textPage); +// } +// +// [Test] +// [TestCase(true)] +// [TestCase(false)] +// public async Task Can_Not_Get_Deleted_Published_Content_By_Id(bool preview) +// { +// // Arrange +// await ContentEditingService.DeleteAsync(PublishedTextPage.Key.Value, Constants.Security.SuperUserKey); +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview); +// +// // Assert +// Assert.IsNull(textPage); +// } +// +// [Test] +// [TestCase(true)] +// [TestCase(false)] +// public async Task Can_Not_Get_Deleted_Published_Content_By_Key(bool preview) +// { +// // Arrange +// await ContentEditingService.DeleteAsync(PublishedTextPage.Key.Value, Constants.Security.SuperUserKey); +// +// // Act +// var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, preview); +// +// // Assert +// Assert.IsNull(textPage); +// } +// +// private void AssertTextPage(IPublishedContent textPage) +// { +// Assert.Multiple(() => +// { +// Assert.IsNotNull(textPage); +// Assert.AreEqual(Textpage.Key, textPage.Key); +// Assert.AreEqual(Textpage.ContentTypeKey, textPage.ContentType.Key); +// Assert.AreEqual(Textpage.InvariantName, textPage.Name); +// }); +// +// AssertProperties(Textpage.InvariantProperties, textPage.Properties); +// } +// +// private void 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) +// { +// AssertProperty(prop, publishedProperties.First(x => x.Alias == prop.Alias)); +// } +// } +// +// private void AssertProperty(PropertyValueModel property, IPublishedProperty publishedProperty) +// { +// Assert.Multiple(() => +// { +// Assert.AreEqual(property.Alias, publishedProperty.Alias); +// Assert.AreEqual(property.Value, publishedProperty.GetSourceValue()); +// }); +// } +// } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs index 34e69c0344..fd13c6678a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs @@ -15,7 +15,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] -[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest { private string _englishIsoCode = "en-US"; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheTests.cs index 63fc6eb841..7a9aaa5d0d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheTests.cs @@ -14,7 +14,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] -[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] public class MediaHybridCacheTests : UmbracoIntegrationTest { private IPublishedMediaCache PublishedMediaHybridCache => GetRequiredService(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MemberHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MemberHybridCacheTests.cs index 9f1c201deb..fae8e5bdb5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MemberHybridCacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MemberHybridCacheTests.cs @@ -13,7 +13,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] -[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] public class MemberHybridCacheTests : UmbracoIntegrationTest { private IPublishedMemberCache PublishedMemberHybridCache => GetRequiredService(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs index 2b25fde0a3..144f987da8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs @@ -7,8 +7,10 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Web; @@ -21,7 +23,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.UrlAndDomains; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Mapper = true, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)] -[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] public class DomainAndUrlsTests : UmbracoIntegrationTest { [SetUp] @@ -72,7 +73,6 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest { builder.Services.AddUnique(_variationContextAccessor); builder.AddUmbracoHybridCache(); - builder.AddNuCache(); // Ensure cache refreshers runs builder.Services.AddUnique(); @@ -412,5 +412,7 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest GetRequiredService(), GetRequiredService>(), GetRequiredService(), - GetRequiredService()).GetAwaiter().GetResult(); + GetRequiredService(), + GetRequiredService(), + GetRequiredService()).GetAwaiter().GetResult(); } diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/Objects/TestUmbracoContextFactory.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/Objects/TestUmbracoContextFactory.cs index 9e663fa9bc..16b38cdd78 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/Objects/TestUmbracoContextFactory.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/Objects/TestUmbracoContextFactory.cs @@ -54,23 +54,21 @@ public class TestUmbracoContextFactory var contentCache = new Mock(); var mediaCache = new Mock(); - var snapshot = new Mock(); - snapshot.Setup(x => x.Content).Returns(contentCache.Object); - snapshot.Setup(x => x.Media).Returns(mediaCache.Object); - var snapshotService = new Mock(); - snapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(snapshot.Object); + var cacheManager = new Mock(); + cacheManager.Setup(x => x.Content).Returns(contentCache.Object); + cacheManager.Setup(x => x.Media).Returns(mediaCache.Object); var hostingEnvironment = TestHelper.GetHostingEnvironment(); var umbracoContextFactory = new UmbracoContextFactory( umbracoContextAccessor, - snapshotService.Object, new UmbracoRequestPaths(Options.Create(globalSettings), hostingEnvironment, Options.Create(umbracoRequestPathsOptions)), hostingEnvironment, new UriUtility(hostingEnvironment), new AspNetCoreCookieManager(httpContextAccessor), httpContextAccessor, - Mock.Of()); + Mock.Of(), + cacheManager.Object); return umbracoContextFactory; } diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs index b4d315a699..571b5929ea 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs @@ -1,295 +1,296 @@ -using System.Collections.Generic; -using System.Data; -using System.Linq; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Logging; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Core.Sync; -using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Cms.Infrastructure.Serialization; -using Umbraco.Cms.Tests.Common; -using Umbraco.Extensions; -using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider; - -namespace Umbraco.Cms.Tests.UnitTests.TestHelpers; - -[TestFixture] -public class PublishedSnapshotServiceTestBase -{ - [SetUp] - public virtual void Setup() - { - VariationContextAccessor = new TestVariationContextAccessor(); - PublishedSnapshotAccessor = new TestPublishedSnapshotAccessor(); - } - - [TearDown] - public void Teardown() => SnapshotService?.Dispose(); - - protected IShortStringHelper ShortStringHelper { get; } = TestHelper.ShortStringHelper; - - protected virtual IPublishedModelFactory PublishedModelFactory { get; } = new NoopPublishedModelFactory(); - - protected IContentTypeService ContentTypeService { get; private set; } - - protected IMediaTypeService MediaTypeService { get; private set; } - - protected IDataTypeService DataTypeService { get; private set; } - - protected IDomainService DomainService { get; private set; } - - protected IPublishedValueFallback PublishedValueFallback { get; private set; } - - protected IPublishedSnapshotService SnapshotService { get; private set; } - - protected IVariationContextAccessor VariationContextAccessor { get; private set; } - - protected TestPublishedSnapshotAccessor PublishedSnapshotAccessor { get; private set; } - - protected TestNuCacheContentService NuCacheContentService { get; private set; } - - protected PublishedContentTypeFactory PublishedContentTypeFactory { get; private set; } - - protected GlobalSettings GlobalSettings { get; } = new(); - - protected virtual PropertyValueConverterCollection PropertyValueConverterCollection => - new(() => new[] { new TestSimpleTinyMceValueConverter() }); - - protected IPublishedContent GetContent(int id) - { - var snapshot = GetPublishedSnapshot(); - var doc = snapshot.Content.GetById(id); - Assert.IsNotNull(doc); - return doc; - } - - protected IPublishedContent GetMedia(int id) - { - var snapshot = GetPublishedSnapshot(); - var doc = snapshot.Media.GetById(id); - Assert.IsNotNull(doc); - return doc; - } - - protected UrlProvider GetUrlProvider( - IUmbracoContextAccessor umbracoContextAccessor, - RequestHandlerSettings requestHandlerSettings, - WebRoutingSettings webRoutingSettings, - out UriUtility uriUtility) - { - uriUtility = new UriUtility(Mock.Of()); - var urlProvider = new DefaultUrlProvider( - Mock.Of>(x => x.CurrentValue == requestHandlerSettings), - Mock.Of>(), - new SiteDomainMapper(), - umbracoContextAccessor, - uriUtility, - Mock.Of(x => x.GetDefaultLanguageIsoCode() == GlobalSettings.DefaultUILanguage)); - - var publishedUrlProvider = new UrlProvider( - umbracoContextAccessor, - Options.Create(webRoutingSettings), - new UrlProviderCollection(() => new[] { urlProvider }), - new MediaUrlProviderCollection(() => Enumerable.Empty()), - Mock.Of()); - - return publishedUrlProvider; - } - - protected static PublishedRouter CreatePublishedRouter( - IUmbracoContextAccessor umbracoContextAccessor, - IEnumerable contentFinders = null, - IPublishedUrlProvider publishedUrlProvider = null, - IDomainCache domainCache = null) - { - return new( - Mock.Of>(x => x.CurrentValue == new WebRoutingSettings()), - new ContentFinderCollection(() => contentFinders ?? Enumerable.Empty()), - new TestLastChanceFinder(), - new TestVariationContextAccessor(), - Mock.Of(), - Mock.Of>(), - publishedUrlProvider ?? Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - umbracoContextAccessor, - Mock.Of(), - domainCache ?? Mock.Of()); - } - - protected IUmbracoContextAccessor GetUmbracoContextAccessor(string urlAsString) - { - var snapshot = GetPublishedSnapshot(); - - var uri = new Uri(urlAsString.Contains(Uri.SchemeDelimiter) - ? urlAsString - : $"http://example.com{urlAsString}"); - - var umbracoContext = Mock.Of( - x => x.CleanedUmbracoUrl == uri - && x.Content == snapshot.Content - && x.PublishedSnapshot == snapshot); - var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); - return umbracoContextAccessor; - } - - /// - /// Used as a property editor for any test property that has an editor alias called "Umbraco.Void.RTE" - /// - private class TestSimpleTinyMceValueConverter : SimpleTinyMceValueConverter - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias == "Umbraco.Void.RTE"; - } - - protected static DataType[] GetDefaultDataTypes() - { - var serializer = new SystemTextConfigurationEditorJsonSerializer(); - - // create data types, property types and content types - var dataType = - new DataType(new VoidEditor("Editor", Mock.Of()), serializer) { Id = 3 }; - - return new[] { dataType }; - } - - protected virtual ServiceContext CreateServiceContext(IContentType[] contentTypes, IMediaType[] mediaTypes, IDataType[] dataTypes) - { - var contentTypeService = new Mock(); - contentTypeService.Setup(x => x.GetAll()).Returns(contentTypes); - contentTypeService.Setup(x => x.GetAll(It.IsAny())).Returns(contentTypes); - contentTypeService.Setup(x => x.Get(It.IsAny())) - .Returns((string alias) => contentTypes.FirstOrDefault(x => x.Alias.InvariantEquals(alias))); - - var mediaTypeService = new Mock(); - mediaTypeService.Setup(x => x.GetAll()).Returns(mediaTypes); - mediaTypeService.Setup(x => x.GetAll(It.IsAny())).Returns(mediaTypes); - mediaTypeService.Setup(x => x.Get(It.IsAny())) - .Returns((string alias) => mediaTypes.FirstOrDefault(x => x.Alias.InvariantEquals(alias))); - - var contentTypeServiceBaseFactory = new Mock(); - contentTypeServiceBaseFactory.Setup(x => x.For(It.IsAny())) - .Returns(contentTypeService.Object); - - var dataTypeServiceMock = new Mock(); - dataTypeServiceMock.Setup(x => x.GetAll()).Returns(dataTypes); - - return ServiceContext.CreatePartial( - dataTypeService: dataTypeServiceMock.Object, - memberTypeService: Mock.Of(), - memberService: Mock.Of(), - contentTypeService: contentTypeService.Object, - mediaTypeService: mediaTypeService.Object, - localizationService: Mock.Of(), - domainService: Mock.Of(), - fileService: Mock.Of()); - } - - /// - /// Creates a published snapshot and set the accessor to resolve the created one - /// - /// - protected IPublishedSnapshot GetPublishedSnapshot() - { - var snapshot = SnapshotService.CreatePublishedSnapshot(null); - PublishedSnapshotAccessor.SetCurrent(snapshot); - return snapshot; - } - - /// - /// Initializes the with a source of data. - /// - protected void InitializedCache( - IEnumerable contentNodeKits, - IContentType[] contentTypes, - IDataType[] dataTypes = null, - IEnumerable mediaNodeKits = null, - IMediaType[] mediaTypes = null) - { - // create a data source for NuCache - NuCacheContentService = new TestNuCacheContentService(contentNodeKits, mediaNodeKits); - - var runtime = Mock.Of(); - Mock.Get(runtime).Setup(x => x.Level).Returns(RuntimeLevel.Run); - - // create a service context - var serviceContext = CreateServiceContext( - contentTypes ?? Array.Empty(), - mediaTypes ?? Array.Empty(), - dataTypes ?? GetDefaultDataTypes()); - - DataTypeService = serviceContext.DataTypeService; - ContentTypeService = serviceContext.ContentTypeService; - MediaTypeService = serviceContext.MediaTypeService; - DomainService = serviceContext.DomainService; - - // create a scope provider - var scopeProvider = Mock.Of(); - Mock.Get(scopeProvider) - .Setup(x => x.CreateScope( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(Mock.Of); - - // create a published content type factory - PublishedContentTypeFactory = new PublishedContentTypeFactory( - PublishedModelFactory, - PropertyValueConverterCollection, - DataTypeService); - - var typeFinder = TestHelper.GetTypeFinder(); - - var nuCacheSettings = new NuCacheSettings(); - - // at last, create the complete NuCache snapshot service! - var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; - SnapshotService = new PublishedSnapshotService( - options, - Mock.Of(x => x.GetSyncBootState() == SyncBootState.WarmBoot), - new SimpleMainDom(), - serviceContext, - PublishedContentTypeFactory, - PublishedSnapshotAccessor, - VariationContextAccessor, - Mock.Of(), - NullLoggerFactory.Instance, - scopeProvider, - NuCacheContentService, - new TestDefaultCultureAccessor(), - Options.Create(GlobalSettings), - PublishedModelFactory, - TestHelper.GetHostingEnvironment(), - Options.Create(nuCacheSettings), - new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); - - // invariant is the current default - VariationContextAccessor.VariationContext = new VariationContext(); - - PublishedValueFallback = new PublishedValueFallback(serviceContext, VariationContextAccessor); - } -} +// using System.Collections.Generic; +// using System.Data; +// using System.Linq; +// using Microsoft.Extensions.Logging; +// using Microsoft.Extensions.Logging.Abstractions; +// using Microsoft.Extensions.Options; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core; +// using Umbraco.Cms.Core.Configuration.Models; +// using Umbraco.Cms.Core.Events; +// using Umbraco.Cms.Core.Hosting; +// using Umbraco.Cms.Core.Logging; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Core.PropertyEditors; +// using Umbraco.Cms.Core.PropertyEditors.ValueConverters; +// using Umbraco.Cms.Core.PublishedCache; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Cms.Core.Scoping; +// using Umbraco.Cms.Core.Services; +// using Umbraco.Cms.Core.Strings; +// using Umbraco.Cms.Core.Sync; +// using Umbraco.Cms.Core.Web; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +// using Umbraco.Cms.Infrastructure.Serialization; +// using Umbraco.Cms.Tests.Common; +// using Umbraco.Extensions; +// using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider; +// +// namespace Umbraco.Cms.Tests.UnitTests.TestHelpers; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public virtual void Setup() +// { +// VariationContextAccessor = new TestVariationContextAccessor(); +// PublishedSnapshotAccessor = new TestPublishedSnapshotAccessor(); +// } +// +// [TearDown] +// public void Teardown() => SnapshotService?.Dispose(); +// +// protected IShortStringHelper ShortStringHelper { get; } = TestHelper.ShortStringHelper; +// +// protected virtual IPublishedModelFactory PublishedModelFactory { get; } = new NoopPublishedModelFactory(); +// +// protected IContentTypeService ContentTypeService { get; private set; } +// +// protected IMediaTypeService MediaTypeService { get; private set; } +// +// protected IDataTypeService DataTypeService { get; private set; } +// +// protected IDomainService DomainService { get; private set; } +// +// protected IPublishedValueFallback PublishedValueFallback { get; private set; } +// +// protected IPublishedSnapshotService SnapshotService { get; private set; } +// +// protected IVariationContextAccessor VariationContextAccessor { get; private set; } +// +// protected TestPublishedSnapshotAccessor PublishedSnapshotAccessor { get; private set; } +// +// protected TestNuCacheContentService NuCacheContentService { get; private set; } +// +// protected PublishedContentTypeFactory PublishedContentTypeFactory { get; private set; } +// +// protected GlobalSettings GlobalSettings { get; } = new(); +// +// protected virtual PropertyValueConverterCollection PropertyValueConverterCollection => +// new(() => new[] { new TestSimpleTinyMceValueConverter() }); +// +// protected IPublishedContent GetContent(int id) +// { +// var snapshot = GetPublishedSnapshot(); +// var doc = snapshot.Content.GetById(id); +// Assert.IsNotNull(doc); +// return doc; +// } +// +// protected IPublishedContent GetMedia(int id) +// { +// var snapshot = GetPublishedSnapshot(); +// var doc = snapshot.Media.GetById(id); +// Assert.IsNotNull(doc); +// return doc; +// } +// +// protected UrlProvider GetUrlProvider( +// IUmbracoContextAccessor umbracoContextAccessor, +// RequestHandlerSettings requestHandlerSettings, +// WebRoutingSettings webRoutingSettings, +// out UriUtility uriUtility) +// { +// uriUtility = new UriUtility(Mock.Of()); +// var urlProvider = new DefaultUrlProvider( +// Mock.Of>(x => x.CurrentValue == requestHandlerSettings), +// Mock.Of>(), +// new SiteDomainMapper(), +// umbracoContextAccessor, +// uriUtility, +// Mock.Of(x => x.GetDefaultLanguageIsoCode() == GlobalSettings.DefaultUILanguage)); +// +// var publishedUrlProvider = new UrlProvider( +// umbracoContextAccessor, +// Options.Create(webRoutingSettings), +// new UrlProviderCollection(() => new[] { urlProvider }), +// new MediaUrlProviderCollection(() => Enumerable.Empty()), +// Mock.Of()); +// +// return publishedUrlProvider; +// } +// +// protected static PublishedRouter CreatePublishedRouter( +// IUmbracoContextAccessor umbracoContextAccessor, +// IEnumerable contentFinders = null, +// IPublishedUrlProvider publishedUrlProvider = null, +// IDomainCache domainCache = null) +// { +// return new( +// Mock.Of>(x => x.CurrentValue == new WebRoutingSettings()), +// new ContentFinderCollection(() => contentFinders ?? Enumerable.Empty()), +// new TestLastChanceFinder(), +// new TestVariationContextAccessor(), +// Mock.Of(), +// Mock.Of>(), +// publishedUrlProvider ?? Mock.Of(), +// Mock.Of(), +// Mock.Of(), +// Mock.Of(), +// Mock.Of(), +// umbracoContextAccessor, +// Mock.Of(), +// domainCache ?? Mock.Of()); +// } +// +// protected IUmbracoContextAccessor GetUmbracoContextAccessor(string urlAsString) +// { +// var snapshot = GetPublishedSnapshot(); +// +// var uri = new Uri(urlAsString.Contains(Uri.SchemeDelimiter) +// ? urlAsString +// : $"http://example.com{urlAsString}"); +// +// var umbracoContext = Mock.Of( +// x => x.CleanedUmbracoUrl == uri +// && x.Content == snapshot.Content +// && x.PublishedSnapshot == snapshot); +// var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); +// return umbracoContextAccessor; +// } +// +// /// +// /// Used as a property editor for any test property that has an editor alias called "Umbraco.Void.RTE" +// /// +// private class TestSimpleTinyMceValueConverter : SimpleTinyMceValueConverter +// { +// public override bool IsConverter(IPublishedPropertyType propertyType) +// => propertyType.EditorAlias == "Umbraco.Void.RTE"; +// } +// +// protected static DataType[] GetDefaultDataTypes() +// { +// var serializer = new SystemTextConfigurationEditorJsonSerializer(); +// +// // create data types, property types and content types +// var dataType = +// new DataType(new VoidEditor("Editor", Mock.Of()), serializer) { Id = 3 }; +// +// return new[] { dataType }; +// } +// +// protected virtual ServiceContext CreateServiceContext(IContentType[] contentTypes, IMediaType[] mediaTypes, IDataType[] dataTypes) +// { +// var contentTypeService = new Mock(); +// contentTypeService.Setup(x => x.GetAll()).Returns(contentTypes); +// contentTypeService.Setup(x => x.GetAll(It.IsAny())).Returns(contentTypes); +// contentTypeService.Setup(x => x.Get(It.IsAny())) +// .Returns((string alias) => contentTypes.FirstOrDefault(x => x.Alias.InvariantEquals(alias))); +// +// var mediaTypeService = new Mock(); +// mediaTypeService.Setup(x => x.GetAll()).Returns(mediaTypes); +// mediaTypeService.Setup(x => x.GetAll(It.IsAny())).Returns(mediaTypes); +// mediaTypeService.Setup(x => x.Get(It.IsAny())) +// .Returns((string alias) => mediaTypes.FirstOrDefault(x => x.Alias.InvariantEquals(alias))); +// +// var contentTypeServiceBaseFactory = new Mock(); +// contentTypeServiceBaseFactory.Setup(x => x.For(It.IsAny())) +// .Returns(contentTypeService.Object); +// +// var dataTypeServiceMock = new Mock(); +// dataTypeServiceMock.Setup(x => x.GetAll()).Returns(dataTypes); +// +// return ServiceContext.CreatePartial( +// dataTypeService: dataTypeServiceMock.Object, +// memberTypeService: Mock.Of(), +// memberService: Mock.Of(), +// contentTypeService: contentTypeService.Object, +// mediaTypeService: mediaTypeService.Object, +// localizationService: Mock.Of(), +// domainService: Mock.Of(), +// fileService: Mock.Of()); +// } +// +// /// +// /// Creates a published snapshot and set the accessor to resolve the created one +// /// +// /// +// protected IPublishedSnapshot GetPublishedSnapshot() +// { +// var snapshot = SnapshotService.CreatePublishedSnapshot(null); +// PublishedSnapshotAccessor.SetCurrent(snapshot); +// return snapshot; +// } +// +// /// +// /// Initializes the with a source of data. +// /// +// protected void InitializedCache( +// IEnumerable contentNodeKits, +// IContentType[] contentTypes, +// IDataType[] dataTypes = null, +// IEnumerable mediaNodeKits = null, +// IMediaType[] mediaTypes = null) +// { +// // create a data source for NuCache +// NuCacheContentService = new TestNuCacheContentService(contentNodeKits, mediaNodeKits); +// +// var runtime = Mock.Of(); +// Mock.Get(runtime).Setup(x => x.Level).Returns(RuntimeLevel.Run); +// +// // create a service context +// var serviceContext = CreateServiceContext( +// contentTypes ?? Array.Empty(), +// mediaTypes ?? Array.Empty(), +// dataTypes ?? GetDefaultDataTypes()); +// +// DataTypeService = serviceContext.DataTypeService; +// ContentTypeService = serviceContext.ContentTypeService; +// MediaTypeService = serviceContext.MediaTypeService; +// DomainService = serviceContext.DomainService; +// +// // create a scope provider +// var scopeProvider = Mock.Of(); +// Mock.Get(scopeProvider) +// .Setup(x => x.CreateScope( +// It.IsAny(), +// It.IsAny(), +// It.IsAny(), +// It.IsAny(), +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .Returns(Mock.Of); +// +// // create a published content type factory +// PublishedContentTypeFactory = new PublishedContentTypeFactory( +// PublishedModelFactory, +// PropertyValueConverterCollection, +// DataTypeService); +// +// var typeFinder = TestHelper.GetTypeFinder(); +// +// var nuCacheSettings = new NuCacheSettings(); +// +// // at last, create the complete NuCache snapshot service! +// var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; +// SnapshotService = new PublishedSnapshotService( +// options, +// Mock.Of(x => x.GetSyncBootState() == SyncBootState.WarmBoot), +// new SimpleMainDom(), +// serviceContext, +// PublishedContentTypeFactory, +// PublishedSnapshotAccessor, +// VariationContextAccessor, +// Mock.Of(), +// NullLoggerFactory.Instance, +// scopeProvider, +// NuCacheContentService, +// new TestDefaultCultureAccessor(), +// Options.Create(GlobalSettings), +// PublishedModelFactory, +// TestHelper.GetHostingEnvironment(), +// Options.Create(nuCacheSettings), +// new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); +// +// // invariant is the current default +// VariationContextAccessor.VariationContext = new VariationContext(); +// +// PublishedValueFallback = new PublishedValueFallback(serviceContext, VariationContextAccessor); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestNuCacheContentService.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestNuCacheContentService.cs index 34f111601a..c58dca9d89 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestNuCacheContentService.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestNuCacheContentService.cs @@ -1,108 +1,109 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; - -namespace Umbraco.Cms.Tests.UnitTests.TestHelpers; - -public class TestNuCacheContentService : INuCacheContentService -{ - public TestNuCacheContentService(params ContentNodeKit[] kits) - : this((IEnumerable)kits) - { - } - - public TestNuCacheContentService( - IEnumerable contentKits, - IEnumerable mediaKits = null) - { - ContentKits = contentKits?.ToDictionary(x => x.Node.Id, x => x) ?? new Dictionary(); - MediaKits = mediaKits?.ToDictionary(x => x.Node.Id, x => x) ?? new Dictionary(); - } - - private IPublishedModelFactory PublishedModelFactory { get; } = new NoopPublishedModelFactory(); - - public Dictionary ContentKits { get; } - - public Dictionary MediaKits { get; } - - // note: it is important to clone the returned kits, as the inner - // ContentNode is directly reused and modified by the snapshot service - public ContentNodeKit GetContentSource(int id) - => ContentKits.TryGetValue(id, out var kit) ? kit.Clone(PublishedModelFactory) : default; - - public IEnumerable GetAllContentSources() - => ContentKits.Values - .OrderBy(x => x.Node.Level) - .ThenBy(x => x.Node.ParentContentId) - .ThenBy(x => x.Node.SortOrder) - .Select(x => x.Clone(PublishedModelFactory)); - - public IEnumerable GetBranchContentSources(int id) - => ContentKits.Values - .Where(x => x.Node.Path.EndsWith("," + id) || x.Node.Path.Contains("," + id + ",")) - .OrderBy(x => x.Node.Level) - .ThenBy(x => x.Node.ParentContentId) - .ThenBy(x => x.Node.SortOrder) - .Select(x => x.Clone(PublishedModelFactory)); - - public IEnumerable GetTypeContentSources(IEnumerable ids) - => ContentKits.Values - .Where(x => ids.Contains(x.ContentTypeId)) - .OrderBy(x => x.Node.Level) - .ThenBy(x => x.Node.ParentContentId) - .ThenBy(x => x.Node.SortOrder) - .Select(x => x.Clone(PublishedModelFactory)); - - public ContentNodeKit GetMediaSource(int id) - => MediaKits.TryGetValue(id, out var kit) ? kit.Clone(PublishedModelFactory) : default; - - public IEnumerable GetAllMediaSources() - => MediaKits.Values - .OrderBy(x => x.Node.Level) - .ThenBy(x => x.Node.ParentContentId) - .ThenBy(x => x.Node.SortOrder) - .Select(x => x.Clone(PublishedModelFactory)); - - public IEnumerable GetBranchMediaSources(int id) - => MediaKits.Values - .Where(x => x.Node.Path.EndsWith("," + id) || x.Node.Path.Contains("," + id + ",")) - .OrderBy(x => x.Node.Level) - .ThenBy(x => x.Node.ParentContentId) - .ThenBy(x => x.Node.SortOrder) - .Select(x => x.Clone(PublishedModelFactory)); - - public IEnumerable GetTypeMediaSources(IEnumerable ids) - => MediaKits.Values - .Where(x => ids.Contains(x.ContentTypeId)) - .OrderBy(x => x.Node.Level) - .ThenBy(x => x.Node.ParentContentId) - .ThenBy(x => x.Node.SortOrder) - .Select(x => x.Clone(PublishedModelFactory)); - - public void DeleteContentItem(IContentBase item) => throw new NotImplementedException(); - - public void DeleteContentItems(IEnumerable items) => throw new NotImplementedException(); - - public void RefreshContent(IContent content) => throw new NotImplementedException(); - - public void RebuildDatabaseCacheIfSerializerChanged() => throw new NotImplementedException(); - - public void RefreshMedia(IMedia media) => throw new NotImplementedException(); - - public void RefreshMember(IMember member) => throw new NotImplementedException(); - - public void Rebuild( - IReadOnlyCollection contentTypeIds = null, - IReadOnlyCollection mediaTypeIds = null, - IReadOnlyCollection memberTypeIds = null) => - throw new NotImplementedException(); - - public bool VerifyContentDbCache() => throw new NotImplementedException(); - - public bool VerifyMediaDbCache() => throw new NotImplementedException(); - - public bool VerifyMemberDbCache() => throw new NotImplementedException(); -} +// using System.Collections.Generic; +// using System.Linq; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; +// +// namespace Umbraco.Cms.Tests.UnitTests.TestHelpers; +// +// FIXME: Reintroduce if relevant +// public class TestNuCacheContentService : INuCacheContentService +// { +// public TestNuCacheContentService(params ContentNodeKit[] kits) +// : this((IEnumerable)kits) +// { +// } +// +// public TestNuCacheContentService( +// IEnumerable contentKits, +// IEnumerable mediaKits = null) +// { +// ContentKits = contentKits?.ToDictionary(x => x.Node.Id, x => x) ?? new Dictionary(); +// MediaKits = mediaKits?.ToDictionary(x => x.Node.Id, x => x) ?? new Dictionary(); +// } +// +// private IPublishedModelFactory PublishedModelFactory { get; } = new NoopPublishedModelFactory(); +// +// public Dictionary ContentKits { get; } +// +// public Dictionary MediaKits { get; } +// +// // note: it is important to clone the returned kits, as the inner +// // ContentNode is directly reused and modified by the snapshot service +// public ContentNodeKit GetContentSource(int id) +// => ContentKits.TryGetValue(id, out var kit) ? kit.Clone(PublishedModelFactory) : default; +// +// public IEnumerable GetAllContentSources() +// => ContentKits.Values +// .OrderBy(x => x.Node.Level) +// .ThenBy(x => x.Node.ParentContentId) +// .ThenBy(x => x.Node.SortOrder) +// .Select(x => x.Clone(PublishedModelFactory)); +// +// public IEnumerable GetBranchContentSources(int id) +// => ContentKits.Values +// .Where(x => x.Node.Path.EndsWith("," + id) || x.Node.Path.Contains("," + id + ",")) +// .OrderBy(x => x.Node.Level) +// .ThenBy(x => x.Node.ParentContentId) +// .ThenBy(x => x.Node.SortOrder) +// .Select(x => x.Clone(PublishedModelFactory)); +// +// public IEnumerable GetTypeContentSources(IEnumerable ids) +// => ContentKits.Values +// .Where(x => ids.Contains(x.ContentTypeId)) +// .OrderBy(x => x.Node.Level) +// .ThenBy(x => x.Node.ParentContentId) +// .ThenBy(x => x.Node.SortOrder) +// .Select(x => x.Clone(PublishedModelFactory)); +// +// public ContentNodeKit GetMediaSource(int id) +// => MediaKits.TryGetValue(id, out var kit) ? kit.Clone(PublishedModelFactory) : default; +// +// public IEnumerable GetAllMediaSources() +// => MediaKits.Values +// .OrderBy(x => x.Node.Level) +// .ThenBy(x => x.Node.ParentContentId) +// .ThenBy(x => x.Node.SortOrder) +// .Select(x => x.Clone(PublishedModelFactory)); +// +// public IEnumerable GetBranchMediaSources(int id) +// => MediaKits.Values +// .Where(x => x.Node.Path.EndsWith("," + id) || x.Node.Path.Contains("," + id + ",")) +// .OrderBy(x => x.Node.Level) +// .ThenBy(x => x.Node.ParentContentId) +// .ThenBy(x => x.Node.SortOrder) +// .Select(x => x.Clone(PublishedModelFactory)); +// +// public IEnumerable GetTypeMediaSources(IEnumerable ids) +// => MediaKits.Values +// .Where(x => ids.Contains(x.ContentTypeId)) +// .OrderBy(x => x.Node.Level) +// .ThenBy(x => x.Node.ParentContentId) +// .ThenBy(x => x.Node.SortOrder) +// .Select(x => x.Clone(PublishedModelFactory)); +// +// public void DeleteContentItem(IContentBase item) => throw new NotImplementedException(); +// +// public void DeleteContentItems(IEnumerable items) => throw new NotImplementedException(); +// +// public void RefreshContent(IContent content) => throw new NotImplementedException(); +// +// public void RebuildDatabaseCacheIfSerializerChanged() => throw new NotImplementedException(); +// +// public void RefreshMedia(IMedia media) => throw new NotImplementedException(); +// +// public void RefreshMember(IMember member) => throw new NotImplementedException(); +// +// public void Rebuild( +// IReadOnlyCollection contentTypeIds = null, +// IReadOnlyCollection mediaTypeIds = null, +// IReadOnlyCollection memberTypeIds = null) => +// throw new NotImplementedException(); +// +// public bool VerifyContentDbCache() => throw new NotImplementedException(); +// +// public bool VerifyMediaDbCache() => throw new NotImplementedException(); +// +// public bool VerifyMemberDbCache() => throw new NotImplementedException(); +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/CacheTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/CacheTests.cs index 194abdc158..500347d896 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/CacheTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/CacheTests.cs @@ -10,8 +10,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; [TestFixture] public class CacheTests : DeliveryApiTests { - [TestCase(PropertyCacheLevel.Snapshot, false, 1)] - [TestCase(PropertyCacheLevel.Snapshot, true, 1)] [TestCase(PropertyCacheLevel.Elements, false, 1)] [TestCase(PropertyCacheLevel.Elements, true, 1)] [TestCase(PropertyCacheLevel.Element, false, 1)] @@ -39,7 +37,7 @@ public class CacheTests : DeliveryApiTests var element = new Mock(); - var prop1 = new PublishedElementPropertyBase(propertyType, element.Object, false, cacheLevel); + var prop1 = new PublishedElementPropertyBase(propertyType, element.Object, false, cacheLevel, Mock.Of()); var results = new List(); results.Add(prop1.GetDeliveryApiValue(expanding)!.ToString()); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs index cbe8ee09b2..ff0cfa2f13 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs @@ -17,8 +17,8 @@ public class ContentBuilderTests : DeliveryApiTests { var content = new Mock(); - var prop1 = new PublishedElementPropertyBase(DeliveryApiPropertyType, content.Object, false, PropertyCacheLevel.None); - var prop2 = new PublishedElementPropertyBase(DefaultPropertyType, content.Object, false, PropertyCacheLevel.None); + var prop1 = new PublishedElementPropertyBase(DeliveryApiPropertyType, content.Object, false, PropertyCacheLevel.None, Mock.Of()); + var prop2 = new PublishedElementPropertyBase(DefaultPropertyType, content.Object, false, PropertyCacheLevel.None, Mock.Of()); var contentType = new Mock(); contentType.SetupGet(c => c.Alias).Returns("thePageType"); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs index 06e22d935d..e88d71f9e1 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs @@ -72,8 +72,8 @@ public class ContentPickerValueConverterTests : PropertyValueConverterTests { var content = new Mock(); - var prop1 = new PublishedElementPropertyBase(DeliveryApiPropertyType, content.Object, false, PropertyCacheLevel.None); - var prop2 = new PublishedElementPropertyBase(DefaultPropertyType, content.Object, false, PropertyCacheLevel.None); + var prop1 = new PublishedElementPropertyBase(DeliveryApiPropertyType, content.Object, false, PropertyCacheLevel.None, Mock.Of()); + var prop2 = new PublishedElementPropertyBase(DefaultPropertyType, content.Object, false, PropertyCacheLevel.None, Mock.Of()); var publishedPropertyType = new Mock(); publishedPropertyType.SetupGet(p => p.Alias).Returns("test"); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs index 8fafb162a2..eca04e10f2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -20,10 +21,12 @@ public class ContentRouteBuilderTests : DeliveryApiTests [TestCase(false)] public void CanBuildForRoot(bool hideTopLevelNodeFromPath) { - var rootKey = Guid.NewGuid(); - var root = SetupInvariantPublishedContent("The Root", rootKey); + var navigationQueryServiceMock = new Mock(); - var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath); + var rootKey = Guid.NewGuid(); + var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); + + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(root); Assert.IsNotNull(result); Assert.AreEqual("/", result.Path); @@ -35,13 +38,19 @@ public class ContentRouteBuilderTests : DeliveryApiTests [TestCase(false)] public void CanBuildForChild(bool hideTopLevelNodeFromPath) { + var navigationQueryServiceMock = new Mock(); + var rootKey = Guid.NewGuid(); - var root = SetupInvariantPublishedContent("The Root", rootKey); + var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); var childKey = Guid.NewGuid(); - var child = SetupInvariantPublishedContent("The Child", childKey, root); + var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); - var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath); + var contentCache = CreatePublishedContentCache("#"); + Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child); Assert.IsNotNull(result); Assert.AreEqual("/the-child", result.Path); @@ -53,16 +62,23 @@ public class ContentRouteBuilderTests : DeliveryApiTests [TestCase(false)] public void CanBuildForGrandchild(bool hideTopLevelNodeFromPath) { + var navigationQueryServiceMock = new Mock(); + var rootKey = Guid.NewGuid(); - var root = SetupInvariantPublishedContent("The Root", rootKey); + var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); var childKey = Guid.NewGuid(); - var child = SetupInvariantPublishedContent("The Child", childKey, root); + var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); var grandchildKey = Guid.NewGuid(); - var grandchild = SetupInvariantPublishedContent("The Grandchild", grandchildKey, child); + var grandchild = SetupInvariantPublishedContent("The Grandchild", grandchildKey, navigationQueryServiceMock, child); - var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath); + var contentCache = CreatePublishedContentCache("#"); + Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(grandchild.Key)).Returns(grandchild); + + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(grandchild); Assert.IsNotNull(result); Assert.AreEqual("/the-child/the-grandchild", result.Path); @@ -73,13 +89,19 @@ public class ContentRouteBuilderTests : DeliveryApiTests [Test] public void CanBuildForCultureVariantRootAndChild() { + var navigationQueryServiceMock = new Mock(); + var rootKey = Guid.NewGuid(); - var root = SetupVariantPublishedContent("The Root", rootKey); + var root = SetupVariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); var childKey = Guid.NewGuid(); - var child = SetupVariantPublishedContent("The Child", childKey, root); + var child = SetupVariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); - var builder = CreateApiContentRouteBuilder(false); + var contentCache = CreatePublishedContentCache("#"); + Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + + var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); Assert.AreEqual("/the-child-en-us", result.Path); @@ -96,13 +118,19 @@ public class ContentRouteBuilderTests : DeliveryApiTests [Test] public void CanBuildForCultureVariantRootAndCultureInvariantChild() { + var navigationQueryServiceMock = new Mock(); + var rootKey = Guid.NewGuid(); - var root = SetupVariantPublishedContent("The Root", rootKey); + var root = SetupVariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); var childKey = Guid.NewGuid(); - var child = SetupInvariantPublishedContent("The Child", childKey, root); + var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); - var builder = CreateApiContentRouteBuilder(false); + var contentCache = CreatePublishedContentCache("#"); + Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + + var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); Assert.AreEqual("/the-child", result.Path); @@ -119,13 +147,19 @@ public class ContentRouteBuilderTests : DeliveryApiTests [Test] public void CanBuildForCultureInvariantRootAndCultureVariantChild() { + var navigationQueryServiceMock = new Mock(); + var rootKey = Guid.NewGuid(); - var root = SetupInvariantPublishedContent("The Root", rootKey); + var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); var childKey = Guid.NewGuid(); - var child = SetupVariantPublishedContent("The Child", childKey, root); + var child = SetupVariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); - var builder = CreateApiContentRouteBuilder(false); + var contentCache = CreatePublishedContentCache("#"); + Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + + var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); Assert.AreEqual("/the-child-en-us", result.Path); @@ -175,17 +209,24 @@ public class ContentRouteBuilderTests : DeliveryApiTests [TestCase(false)] public void VerifyPublishedUrlProviderSetup(bool hideTopLevelNodeFromPath) { + var navigationQueryServiceMock = new Mock(); + var rootKey = Guid.NewGuid(); - var root = SetupInvariantPublishedContent("The Root", rootKey); + var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); var childKey = Guid.NewGuid(); - var child = SetupInvariantPublishedContent("The Child", childKey, root); + var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); var grandchildKey = Guid.NewGuid(); - var grandchild = SetupInvariantPublishedContent("The Grandchild", grandchildKey, child); + var grandchild = SetupInvariantPublishedContent("The Grandchild", grandchildKey, navigationQueryServiceMock, child); + + var contentCache = Mock.Of(); + Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(grandchild.Key)).Returns(grandchild); // yes... actually testing the mock setup here. but it's important for the rest of the tests that this behave correct, so we better test it. - var publishedUrlProvider = SetupPublishedUrlProvider(hideTopLevelNodeFromPath); + var publishedUrlProvider = SetupPublishedUrlProvider(hideTopLevelNodeFromPath, contentCache, navigationQueryServiceMock.Object); Assert.AreEqual(hideTopLevelNodeFromPath ? "/" : "/the-root", publishedUrlProvider.GetUrl(root)); Assert.AreEqual(hideTopLevelNodeFromPath ? "/the-child" : "/the-root/the-child", publishedUrlProvider.GetUrl(child)); Assert.AreEqual(hideTopLevelNodeFromPath ? "/the-child/the-grandchild" : "/the-root/the-child/the-grandchild", publishedUrlProvider.GetUrl(grandchild)); @@ -195,13 +236,22 @@ public class ContentRouteBuilderTests : DeliveryApiTests [TestCase(false)] public void CanRouteUnpublishedChild(bool hideTopLevelNodeFromPath) { + var navigationQueryServiceMock = new Mock(); + var rootKey = Guid.NewGuid(); - var root = SetupInvariantPublishedContent("The Root", rootKey); + var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); + + IEnumerable rootKeys = rootKey.Yield(); + navigationQueryServiceMock.Setup(x => x.TryGetRootKeys(out rootKeys)).Returns(true); var childKey = Guid.NewGuid(); - var child = SetupInvariantPublishedContent("The Child", childKey, root, false); + var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root, false); - var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, isPreview: true); + var contentCache = CreatePublishedContentCache("#"); + Mock.Get(contentCache).Setup(x => x.GetById(true, root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(true, child.Key)).Returns(child); + + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, isPreview: true, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child); Assert.IsNotNull(result); Assert.AreEqual($"/{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}{childKey:D}", result.Path); @@ -213,13 +263,22 @@ public class ContentRouteBuilderTests : DeliveryApiTests [TestCase(false)] public void UnpublishedChildRouteRespectsTrailingSlashSettings(bool addTrailingSlash) { + var navigationQueryServiceMock = new Mock(); + var rootKey = Guid.NewGuid(); - var root = SetupInvariantPublishedContent("The Root", rootKey); + var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); + + IEnumerable rootKeys = rootKey.Yield(); + navigationQueryServiceMock.Setup(x => x.TryGetRootKeys(out rootKeys)).Returns(true); var childKey = Guid.NewGuid(); - var child = SetupInvariantPublishedContent("The Child", childKey, root, false); + var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root, false); - var builder = CreateApiContentRouteBuilder(true, addTrailingSlash, isPreview: true); + var contentCache = CreatePublishedContentCache("#"); + Mock.Get(contentCache).Setup(x => x.GetById(true, root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(true, child.Key)).Returns(child); + + var builder = CreateApiContentRouteBuilder(true, addTrailingSlash, contentCache: contentCache, isPreview: true, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child); Assert.IsNotNull(result); Assert.AreEqual(addTrailingSlash, result.Path.EndsWith("/")); @@ -229,16 +288,25 @@ public class ContentRouteBuilderTests : DeliveryApiTests [TestCase(false)] public void CanRoutePublishedChildOfUnpublishedParentInPreview(bool isPreview) { + var navigationQueryServiceMock = new Mock(); + var rootKey = Guid.NewGuid(); - var root = SetupInvariantPublishedContent("The Root", rootKey, published: false); + var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock, published: false); + + IEnumerable rootKeys = rootKey.Yield(); + navigationQueryServiceMock.Setup(x => x.TryGetRootKeys(out rootKeys)).Returns(true); var childKey = Guid.NewGuid(); - var child = SetupInvariantPublishedContent("The Child", childKey, root); + var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); var requestPreviewServiceMock = new Mock(); requestPreviewServiceMock.Setup(m => m.IsPreview()).Returns(isPreview); - var builder = CreateApiContentRouteBuilder(true, isPreview: isPreview); + var contentCache = CreatePublishedContentCache("#"); + Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + + var builder = CreateApiContentRouteBuilder(true, contentCache: contentCache, isPreview: isPreview, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child); if (isPreview) @@ -257,18 +325,24 @@ public class ContentRouteBuilderTests : DeliveryApiTests [Test] public void CanUseCustomContentPathProvider() { + var navigationQueryServiceMock = new Mock(); + var rootKey = Guid.NewGuid(); - var root = SetupInvariantPublishedContent("The Root", rootKey, published: false); + var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock, published: false); var childKey = Guid.NewGuid(); - var child = SetupInvariantPublishedContent("The Child", childKey, root); + var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); + + var contentCache = CreatePublishedContentCache("#"); + Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); var apiContentPathProvider = new Mock(); apiContentPathProvider .Setup(p => p.GetContentPath(It.IsAny(), It.IsAny())) .Returns((IPublishedContent content, string? culture) => $"my-custom-path-for-{content.UrlSegment}"); - var builder = CreateApiContentRouteBuilder(true, apiContentPathProvider: apiContentPathProvider.Object); + var builder = CreateApiContentRouteBuilder(true, contentCache: contentCache, apiContentPathProvider: apiContentPathProvider.Object, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(root); Assert.NotNull(result); Assert.AreEqual("/my-custom-path-for-the-root", result.Path); @@ -282,18 +356,18 @@ public class ContentRouteBuilderTests : DeliveryApiTests Assert.AreEqual("the-root", result.StartItem.Path); } - private IPublishedContent SetupInvariantPublishedContent(string name, Guid key, IPublishedContent? parent = null, bool published = true) + private IPublishedContent SetupInvariantPublishedContent(string name, Guid key, Mock navigationQueryServiceMock, IPublishedContent? parent = null, bool published = true) { var publishedContentType = CreatePublishedContentType(); - var content = CreatePublishedContentMock(publishedContentType.Object, name, key, parent, published); + var content = CreatePublishedContentMock(publishedContentType.Object, name, key, parent, published, navigationQueryServiceMock); return content.Object; } - private IPublishedContent SetupVariantPublishedContent(string name, Guid key, IPublishedContent? parent = null, bool published = true) + private IPublishedContent SetupVariantPublishedContent(string name, Guid key, Mock navigationQueryServiceMock, IPublishedContent? parent = null, bool published = true) { var publishedContentType = CreatePublishedContentType(); publishedContentType.SetupGet(m => m.Variations).Returns(ContentVariation.Culture); - var content = CreatePublishedContentMock(publishedContentType.Object, name, key, parent, published); + var content = CreatePublishedContentMock(publishedContentType.Object, name, key, parent, published, navigationQueryServiceMock); var cultures = new[] { "en-us", "da-dk" }; content .SetupGet(m => m.Cultures) @@ -303,12 +377,15 @@ public class ContentRouteBuilderTests : DeliveryApiTests return content.Object; } - private Mock CreatePublishedContentMock(IPublishedContentType publishedContentType, string name, Guid key, IPublishedContent? parent, bool published) + private Mock CreatePublishedContentMock(IPublishedContentType publishedContentType, string name, Guid key, IPublishedContent? parent, bool published, Mock navigationQueryServiceMock) { var content = new Mock(); ConfigurePublishedContentMock(content, key, name, DefaultUrlSegment(name), publishedContentType, Array.Empty()); content.Setup(c => c.IsPublished(It.IsAny())).Returns(published); - content.SetupGet(c => c.Parent).Returns(parent); + + Guid? parentKey = parent?.Key; + navigationQueryServiceMock.Setup(x => x.TryGetParentKey(key, out parentKey)).Returns(true); + content.SetupGet(c => c.Level).Returns((parent?.Level ?? 0) + 1); return content; } @@ -321,13 +398,15 @@ public class ContentRouteBuilderTests : DeliveryApiTests return publishedContentType; } - private IPublishedUrlProvider SetupPublishedUrlProvider(bool hideTopLevelNodeFromPath) + private IPublishedUrlProvider SetupPublishedUrlProvider(bool hideTopLevelNodeFromPath, IPublishedContentCache contentCache, IDocumentNavigationQueryService navigationQueryService) { var variantContextAccessor = Mock.Of(); + string Url(IPublishedContent content, string? culture) { - return content.AncestorsOrSelf().All(c => c.IsPublished(culture)) - ? string.Join("/", content.AncestorsOrSelf().Reverse().Skip(hideTopLevelNodeFromPath ? 1 : 0).Select(c => c.UrlSegment(variantContextAccessor, culture))).EnsureStartsWith("/") + var ancestorsOrSelf = content.AncestorsOrSelf(contentCache, navigationQueryService).ToArray(); + return ancestorsOrSelf.All(c => c.IsPublished(culture)) + ? string.Join("/", ancestorsOrSelf.Reverse().Skip(hideTopLevelNodeFromPath ? 1 : 0).Select(c => c.UrlSegment(variantContextAccessor, culture))).EnsureStartsWith("/") : "#"; } @@ -338,10 +417,10 @@ public class ContentRouteBuilderTests : DeliveryApiTests return publishedUrlProvider.Object; } - private IApiContentPathProvider SetupApiContentPathProvider(bool hideTopLevelNodeFromPath) - => new ApiContentPathProvider(SetupPublishedUrlProvider(hideTopLevelNodeFromPath)); + private IApiContentPathProvider SetupApiContentPathProvider(bool hideTopLevelNodeFromPath, IPublishedContentCache contentCache, IDocumentNavigationQueryService navigationQueryService) + => new ApiContentPathProvider(SetupPublishedUrlProvider(hideTopLevelNodeFromPath, contentCache, navigationQueryService)); - private ApiContentRouteBuilder CreateApiContentRouteBuilder(bool hideTopLevelNodeFromPath, bool addTrailingSlash = false, bool isPreview = false, IPublishedSnapshotAccessor? publishedSnapshotAccessor = null, IApiContentPathProvider? apiContentPathProvider = null) + private ApiContentRouteBuilder CreateApiContentRouteBuilder(bool hideTopLevelNodeFromPath, bool addTrailingSlash = false, bool isPreview = false, IPublishedContentCache? contentCache = null, IApiContentPathProvider? apiContentPathProvider = null, IDocumentNavigationQueryService navigationQueryService = null) { var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = addTrailingSlash }; var requestHandlerSettingsMonitorMock = new Mock>(); @@ -350,15 +429,16 @@ public class ContentRouteBuilderTests : DeliveryApiTests var requestPreviewServiceMock = new Mock(); requestPreviewServiceMock.Setup(m => m.IsPreview()).Returns(isPreview); - publishedSnapshotAccessor ??= CreatePublishedSnapshotAccessorForRoute("#"); - apiContentPathProvider ??= SetupApiContentPathProvider(hideTopLevelNodeFromPath); + contentCache ??= CreatePublishedContentCache("#"); + apiContentPathProvider ??= SetupApiContentPathProvider(hideTopLevelNodeFromPath, contentCache, navigationQueryService); return CreateContentRouteBuilder( apiContentPathProvider, CreateGlobalSettings(hideTopLevelNodeFromPath), requestHandlerSettingsMonitor: requestHandlerSettingsMonitorMock.Object, requestPreviewService: requestPreviewServiceMock.Object, - publishedSnapshotAccessor: publishedSnapshotAccessor); + contentCache: contentCache, + navigationQueryService: navigationQueryService); } private IApiContentRoute? GetUnRoutableRoute(string publishedUrl, string routeById) @@ -369,35 +449,25 @@ public class ContentRouteBuilderTests : DeliveryApiTests .Returns(publishedUrl); var contentPathProvider = new ApiContentPathProvider(publishedUrlProviderMock.Object); - var publishedSnapshotAccessor = CreatePublishedSnapshotAccessorForRoute(routeById); - var content = SetupVariantPublishedContent("The Content", Guid.NewGuid()); + var contentCache = CreatePublishedContentCache(routeById); + var navigationQueryServiceMock = new Mock(); + var content = SetupVariantPublishedContent("The Content", Guid.NewGuid(), navigationQueryServiceMock); var builder = CreateContentRouteBuilder( contentPathProvider, CreateGlobalSettings(), - publishedSnapshotAccessor: publishedSnapshotAccessor); + contentCache: contentCache); return builder.Build(content); } - private IPublishedSnapshotAccessor CreatePublishedSnapshotAccessorForRoute(string routeById) + private IPublishedContentCache CreatePublishedContentCache(string routeById) { var publishedContentCacheMock = new Mock(); publishedContentCacheMock .Setup(c => c.GetRouteById(It.IsAny(), It.IsAny())) .Returns(routeById); - var publishedSnapshotMock = new Mock(); - publishedSnapshotMock - .SetupGet(s => s.Content) - .Returns(publishedContentCacheMock.Object); - var publishedSnapshot = publishedSnapshotMock.Object; - - var publishedSnapshotAccessorMock = new Mock(); - publishedSnapshotAccessorMock - .Setup(a => a.TryGetPublishedSnapshot(out publishedSnapshot)) - .Returns(true); - - return publishedSnapshotAccessorMock.Object; + return publishedContentCacheMock.Object; } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs index 47a7c032c9..f048cd357e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs @@ -9,7 +9,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.DeliveryApi; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -117,9 +117,10 @@ public class DeliveryApiTests IApiContentPathProvider contentPathProvider, IOptions globalSettings, IVariationContextAccessor? variationContextAccessor = null, - IPublishedSnapshotAccessor? publishedSnapshotAccessor = null, IRequestPreviewService? requestPreviewService = null, - IOptionsMonitor? requestHandlerSettingsMonitor = null) + IOptionsMonitor? requestHandlerSettingsMonitor = null, + IPublishedContentCache? contentCache = null, + IDocumentNavigationQueryService? navigationQueryService = null) { if (requestHandlerSettingsMonitor == null) { @@ -132,8 +133,9 @@ public class DeliveryApiTests contentPathProvider, globalSettings, variationContextAccessor ?? Mock.Of(), - publishedSnapshotAccessor ?? Mock.Of(), requestPreviewService ?? Mock.Of(), - requestHandlerSettingsMonitor); + requestHandlerSettingsMonitor, + contentCache ?? Mock.Of(), + navigationQueryService ?? Mock.Of()); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MediaPickerWithCropsValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MediaPickerWithCropsValueConverterTests.cs index c1a0f6a7c2..5c02e4b743 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MediaPickerWithCropsValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MediaPickerWithCropsValueConverterTests.cs @@ -27,7 +27,7 @@ public class MediaPickerWithCropsValueConverterTests : PropertyValueConverterTes CreateOutputExpansionStrategyAccessor()), publishedValueFallback); return new MediaPickerWithCropsValueConverter( - PublishedSnapshotAccessor, + CacheManager.Media, PublishedUrlProvider, publishedValueFallback, serializer, diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs index 1948756e44..cfbd1e5495 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs @@ -23,11 +23,13 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest var apiUrProvider = new ApiMediaUrlProvider(PublishedUrlProvider); routeBuilder = routeBuilder ?? CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()); return new MultiNodeTreePickerValueConverter( - PublishedSnapshotAccessor, Mock.Of(), Mock.Of(), new ApiContentBuilder(contentNameProvider, routeBuilder, expansionStrategyAccessor), - new ApiMediaBuilder(contentNameProvider, apiUrProvider, Mock.Of(), expansionStrategyAccessor)); + new ApiMediaBuilder(contentNameProvider, apiUrProvider, Mock.Of(), expansionStrategyAccessor), + CacheManager.Content, + CacheManager.Media, + CacheManager.Members); } private PublishedDataType MultiNodePickerPublishedDataType(bool multiSelect, string entityType) => @@ -99,8 +101,8 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest { var content = new Mock(); - var prop1 = new PublishedElementPropertyBase(DeliveryApiPropertyType, content.Object, false, PropertyCacheLevel.None); - var prop2 = new PublishedElementPropertyBase(DefaultPropertyType, content.Object, false, PropertyCacheLevel.None); + var prop1 = new PublishedElementPropertyBase(DeliveryApiPropertyType, content.Object, false, PropertyCacheLevel.None, CacheManager); + var prop2 = new PublishedElementPropertyBase(DefaultPropertyType, content.Object, false, PropertyCacheLevel.None, CacheManager); var key = Guid.NewGuid(); var urlSegment = "page-url-segment"; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs index f4a565ec4f..4547078d15 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs @@ -296,14 +296,14 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests { var routeBuilder = CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()); return new MultiUrlPickerValueConverter( - PublishedSnapshotAccessor, Mock.Of(), Serializer(), - Mock.Of(), PublishedUrlProvider, new ApiContentNameProvider(), ApiMediaUrlProvider(), - routeBuilder); + routeBuilder, + CacheManager.Content, + CacheManager.Media); } private IJsonSerializer Serializer() => new SystemTextJsonSerializer(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs index 75a331bd31..fe8b45db51 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs @@ -46,8 +46,8 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor); var content = new Mock(); - var prop1 = new PublishedElementPropertyBase(DeliveryApiPropertyType, content.Object, false, PropertyCacheLevel.None); - var prop2 = new PublishedElementPropertyBase(DefaultPropertyType, content.Object, false, PropertyCacheLevel.None); + var prop1 = new PublishedElementPropertyBase(DeliveryApiPropertyType, content.Object, false, PropertyCacheLevel.None, CacheManager); + var prop2 = new PublishedElementPropertyBase(DefaultPropertyType, content.Object, false, PropertyCacheLevel.None, CacheManager); var contentPickerContent = CreateSimplePickedContent(123, 456); var contentPickerProperty = CreateContentPickerProperty(content.Object, contentPickerContent.Key, "contentPicker", apiContentBuilder); @@ -303,7 +303,7 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe .Returns(expanding ? "Expanding" : "Not expanding"); var propertyType = SetupPublishedPropertyType(valueConverterMock.Object, "theAlias", Constants.PropertyEditors.Aliases.Label); - var property = new PublishedElementPropertyBase(propertyType, content.Object, false, PropertyCacheLevel.None, "The Value"); + var property = new PublishedElementPropertyBase(propertyType, content.Object, false, PropertyCacheLevel.None, CacheManager, "The Value"); SetupContentMock(content, property); @@ -378,7 +378,7 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe ContentPickerValueConverter contentPickerValueConverter = new ContentPickerValueConverter(PublishedContentCacheMock.Object, contentBuilder); var contentPickerPropertyType = SetupPublishedPropertyType(contentPickerValueConverter, propertyTypeAlias, Constants.PropertyEditors.Aliases.ContentPicker); - return new PublishedElementPropertyBase(contentPickerPropertyType, parent, false, PropertyCacheLevel.None, new GuidUdi(Constants.UdiEntityType.Document, pickedContentKey).ToString()); + return new PublishedElementPropertyBase(contentPickerPropertyType, parent, false, PropertyCacheLevel.None, CacheManager, new GuidUdi(Constants.UdiEntityType.Document, pickedContentKey).ToString()); } internal PublishedElementPropertyBase CreateMediaPickerProperty(IPublishedElement parent, Guid pickedMediaKey, string propertyTypeAlias, IApiMediaBuilder mediaBuilder) @@ -386,10 +386,10 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe var publishedValueFallback = Mock.Of(); var apiMediaWithCropsBuilder = new ApiMediaWithCropsBuilder(mediaBuilder, publishedValueFallback); - MediaPickerWithCropsValueConverter mediaPickerValueConverter = new MediaPickerWithCropsValueConverter(PublishedSnapshotAccessor, PublishedUrlProvider, publishedValueFallback, new SystemTextJsonSerializer(), apiMediaWithCropsBuilder); + MediaPickerWithCropsValueConverter mediaPickerValueConverter = new MediaPickerWithCropsValueConverter(CacheManager.Media, PublishedUrlProvider, publishedValueFallback, new SystemTextJsonSerializer(), apiMediaWithCropsBuilder); var mediaPickerPropertyType = SetupPublishedPropertyType(mediaPickerValueConverter, propertyTypeAlias, Constants.PropertyEditors.Aliases.MediaPicker3, new MediaPicker3Configuration()); - return new PublishedElementPropertyBase(mediaPickerPropertyType, parent, false, PropertyCacheLevel.None, new GuidUdi(Constants.UdiEntityType.Media, pickedMediaKey).ToString()); + return new PublishedElementPropertyBase(mediaPickerPropertyType, parent, false, PropertyCacheLevel.None, CacheManager, new GuidUdi(Constants.UdiEntityType.Media, pickedMediaKey).ToString()); } internal PublishedElementPropertyBase CreateMediaPicker3Property(IPublishedElement parent, Guid pickedMediaKey, string propertyTypeAlias, IApiMediaBuilder mediaBuilder) @@ -406,16 +406,16 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe var publishedValueFallback = Mock.Of(); var apiMediaWithCropsBuilder = new ApiMediaWithCropsBuilder(mediaBuilder, publishedValueFallback); - MediaPickerWithCropsValueConverter mediaPickerValueConverter = new MediaPickerWithCropsValueConverter(PublishedSnapshotAccessor, PublishedUrlProvider, publishedValueFallback, new SystemTextJsonSerializer(), apiMediaWithCropsBuilder); + MediaPickerWithCropsValueConverter mediaPickerValueConverter = new MediaPickerWithCropsValueConverter(CacheManager.Media, PublishedUrlProvider, publishedValueFallback, new SystemTextJsonSerializer(), apiMediaWithCropsBuilder); var mediaPickerPropertyType = SetupPublishedPropertyType(mediaPickerValueConverter, propertyTypeAlias, Constants.PropertyEditors.Aliases.MediaPicker3, new MediaPicker3Configuration()); - return new PublishedElementPropertyBase(mediaPickerPropertyType, parent, false, PropertyCacheLevel.None, value); + return new PublishedElementPropertyBase(mediaPickerPropertyType, parent, false, PropertyCacheLevel.None, CacheManager, value); } internal PublishedElementPropertyBase CreateNumberProperty(IPublishedElement parent, int propertyValue, string propertyTypeAlias) { var numberPropertyType = SetupPublishedPropertyType(new IntegerValueConverter(), propertyTypeAlias, Constants.PropertyEditors.Aliases.Label); - return new PublishedElementPropertyBase(numberPropertyType, parent, false, PropertyCacheLevel.None, propertyValue); + return new PublishedElementPropertyBase(numberPropertyType, parent, false, PropertyCacheLevel.None, CacheManager, propertyValue); } internal PublishedElementPropertyBase CreateElementProperty( @@ -452,7 +452,7 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe elementValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevelForExpansion(It.IsAny())).Returns(PropertyCacheLevel.None); var elementPropertyType = SetupPublishedPropertyType(elementValueConverter.Object, elementPropertyAlias, "My.Element.Property"); - return new PublishedElementPropertyBase(elementPropertyType, parent, false, PropertyCacheLevel.None); + return new PublishedElementPropertyBase(elementPropertyType, parent, false, PropertyCacheLevel.None, CacheManager); } protected IApiContentRouteBuilder ApiContentRouteBuilder() => CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs index 475945c9df..7050ccada5 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; public class PropertyValueConverterTests : DeliveryApiTests { - protected IPublishedSnapshotAccessor PublishedSnapshotAccessor { get; private set; } + protected ICacheManager CacheManager { get; private set; } protected IPublishedUrlProvider PublishedUrlProvider { get; private set; } @@ -60,9 +60,10 @@ public class PropertyValueConverterTests : DeliveryApiTests .Setup(pcc => pcc.GetById(mediaKey)) .Returns(publishedMedia.Object); - var publishedSnapshot = new Mock(); - publishedSnapshot.SetupGet(ps => ps.Content).Returns(PublishedContentCacheMock.Object); - publishedSnapshot.SetupGet(ps => ps.Media).Returns(PublishedMediaCacheMock.Object); + var cacheMock = new Mock(); + cacheMock.SetupGet(cache => cache.Content).Returns(PublishedContentCacheMock.Object); + cacheMock.SetupGet(cache => cache.Media).Returns(PublishedMediaCacheMock.Object); + CacheManager = cacheMock.Object; PublishedUrlProviderMock = new Mock(); PublishedUrlProviderMock @@ -73,13 +74,6 @@ public class PropertyValueConverterTests : DeliveryApiTests .Returns("the-media-url"); PublishedUrlProvider = PublishedUrlProviderMock.Object; ApiContentPathProvider = new ApiContentPathProvider(PublishedUrlProvider); - - var publishedSnapshotAccessor = new Mock(); - var publishedSnapshotObject = publishedSnapshot.Object; - publishedSnapshotAccessor - .Setup(psa => psa.TryGetPublishedSnapshot(out publishedSnapshotObject)) - .Returns(true); - PublishedSnapshotAccessor = publishedSnapshotAccessor.Object; } protected Mock SetupPublishedContent(string name, Guid key, PublishedItemType itemType, IPublishedContentType contentType) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedContentCacheTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedContentCacheTests.cs index b3f0abe5e1..1480210534 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedContentCacheTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedContentCacheTests.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.HybridCache; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -16,7 +17,7 @@ public class PublishedContentCacheTests : DeliveryApiTests private readonly Guid _contentTwoId = Guid.Parse("4EF11E1E-FB50-4627-8A86-E10ED6F4DCE4"); - private IPublishedSnapshotAccessor _publishedSnapshotAccessor = null!; + private IPublishedContentCache _contentCache; private IPublishedContentCache _contentCacheMock; private IDocumentUrlService _documentUrlService; @@ -55,14 +56,7 @@ public class PublishedContentCacheTests : DeliveryApiTests .Setup(m => m.GetById(It.IsAny(), _contentTwoId)) .Returns(contentTwoMock.Object); - var publishedSnapshotMock = new Mock(); - publishedSnapshotMock.Setup(m => m.Content).Returns(contentCacheMock.Object); - - var publishedSnapshot = publishedSnapshotMock.Object; - var publishedSnapshotAccessorMock = new Mock(); - publishedSnapshotAccessorMock.Setup(m => m.TryGetPublishedSnapshot(out publishedSnapshot)).Returns(true); - - _publishedSnapshotAccessor = publishedSnapshotAccessorMock.Object; + _contentCache = contentCacheMock.Object; _contentCacheMock = contentCacheMock.Object; _documentUrlService = documentUrlService.Object; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs index 88f76dc7f3..a427046135 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs @@ -452,28 +452,30 @@ public class RichTextParserTests : PropertyValueConverterTests private ApiRichTextElementParser CreateRichTextElementParser() { - SetupTestContent(out var routeBuilder, out var snapshotAccessor, out var urlProvider); + SetupTestContent(out var routeBuilder, out var cacheManager, out var urlProvider); return new ApiRichTextElementParser( routeBuilder, urlProvider, - snapshotAccessor, + cacheManager.Content, + cacheManager.Media, new ApiElementBuilder(CreateOutputExpansionStrategyAccessor()), Mock.Of>()); } private ApiRichTextMarkupParser CreateRichTextMarkupParser() { - SetupTestContent(out var routeBuilder, out var snapshotAccessor, out var urlProvider); + SetupTestContent(out var routeBuilder, out var cacheManager, out var urlProvider); return new ApiRichTextMarkupParser( routeBuilder, urlProvider, - snapshotAccessor, + cacheManager.Content, + cacheManager.Media, Mock.Of>()); } - private void SetupTestContent(out IApiContentRouteBuilder routeBuilder, out IPublishedSnapshotAccessor snapshotAccessor, out IApiMediaUrlProvider apiMediaUrlProvider) + private void SetupTestContent(out IApiContentRouteBuilder routeBuilder, out ICacheManager cacheManager, out IApiMediaUrlProvider apiMediaUrlProvider) { var contentMock = new Mock(); contentMock.SetupGet(m => m.Key).Returns(_contentKey); @@ -484,17 +486,14 @@ public class RichTextParserTests : PropertyValueConverterTests mediaMock.SetupGet(m => m.ItemType).Returns(PublishedItemType.Media); var contentCacheMock = new Mock(); - contentCacheMock.Setup(m => m.GetById(new GuidUdi(Constants.UdiEntityType.Document, _contentKey))).Returns(contentMock.Object); + contentCacheMock.Setup(m => m.GetById(_contentKey)).Returns(contentMock.Object); var mediaCacheMock = new Mock(); - mediaCacheMock.Setup(m => m.GetById(new GuidUdi(Constants.UdiEntityType.Media, _mediaKey))).Returns(mediaMock.Object); + mediaCacheMock.Setup(m => m.GetById(_mediaKey)).Returns(mediaMock.Object); - var snapshotMock = new Mock(); - snapshotMock.SetupGet(m => m.Content).Returns(contentCacheMock.Object); - snapshotMock.SetupGet(m => m.Media).Returns(mediaCacheMock.Object); - - var snapshot = snapshotMock.Object; - var snapshotAccessorMock = new Mock(); - snapshotAccessorMock.Setup(m => m.TryGetPublishedSnapshot(out snapshot)).Returns(true); + var cacheManagerMock = new Mock(); + cacheManagerMock.SetupGet(m => m.Content).Returns(contentCacheMock.Object); + cacheManagerMock.SetupGet(m => m.Media).Returns(mediaCacheMock.Object); + cacheManager = cacheManagerMock.Object; var routeBuilderMock = new Mock(); routeBuilderMock @@ -507,7 +506,6 @@ public class RichTextParserTests : PropertyValueConverterTests .Returns("/some-media-url"); routeBuilder = routeBuilderMock.Object; - snapshotAccessor = snapshotAccessorMock.Object; apiMediaUrlProvider = apiMediaUrlProviderMock.Object; } @@ -522,7 +520,7 @@ public class RichTextParserTests : PropertyValueConverterTests element.SetupGet(c => c.ContentType).Returns(elementType.Object); var numberPropertyType = SetupPublishedPropertyType(new IntegerValueConverter(), "number", Constants.PropertyEditors.Aliases.Label); - var property = new PublishedElementPropertyBase(numberPropertyType, element.Object, false, PropertyCacheLevel.None, propertyValue); + var property = new PublishedElementPropertyBase(numberPropertyType, element.Object, false, PropertyCacheLevel.None, CacheManager, propertyValue); element.SetupGet(c => c.Properties).Returns(new[] { property }); return element.Object; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs index 6b090ed7c8..14a02d88c4 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Serialization; @@ -185,12 +186,11 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT private BlockGridPropertyValueConverter CreateConverter() { - var publishedSnapshotAccessor = GetPublishedSnapshotAccessor(); var publishedModelFactory = new NoopPublishedModelFactory(); var blockVarianceHandler = new BlockEditorVarianceHandler(Mock.Of()); var editor = new BlockGridPropertyValueConverter( Mock.Of(), - new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory, Mock.Of(), blockVarianceHandler), + new BlockEditorConverter(GetPublishedContentTypeCache(), Mock.Of(), publishedModelFactory, Mock.Of(), blockVarianceHandler), new SystemTextJsonSerializer(), new ApiElementBuilder(Mock.Of()), new BlockGridPropertyValueConstructorCache(), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs index db720fca46..84070ffe4f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Serialization; @@ -22,12 +23,11 @@ public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterT private BlockListPropertyValueConverter CreateConverter() { - var publishedSnapshotAccessor = GetPublishedSnapshotAccessor(); var publishedModelFactory = new NoopPublishedModelFactory(); var blockVarianceHandler = new BlockEditorVarianceHandler(Mock.Of()); var editor = new BlockListPropertyValueConverter( Mock.Of(), - new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory, Mock.Of(), blockVarianceHandler), + new BlockEditorConverter(GetPublishedContentTypeCache(), Mock.Of(), publishedModelFactory, Mock.Of(), blockVarianceHandler), Mock.Of(), new ApiElementBuilder(Mock.Of()), new SystemTextJsonSerializer(), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs index 1e2739c7ac..6d9efe47d3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs @@ -21,11 +21,7 @@ public abstract class BlockPropertyValueConverterTestsBase - /// Setup mocks for IPublishedSnapshotAccessor - /// - protected IPublishedSnapshotAccessor GetPublishedSnapshotAccessor() + protected IPublishedContentTypeCache GetPublishedContentTypeCache() { var test1ContentType = Mock.Of(x => x.IsElement == true @@ -43,15 +39,14 @@ public abstract class BlockPropertyValueConverterTestsBase(); - contentCache.Setup(x => x.GetContentType(ContentKey1)).Returns(test1ContentType); - contentCache.Setup(x => x.GetContentType(ContentKey2)).Returns(test2ContentType); - contentCache.Setup(x => x.GetContentType(SettingKey1)).Returns(test3ContentType); - contentCache.Setup(x => x.GetContentType(SettingKey2)).Returns(test4ContentType); - var publishedSnapshot = Mock.Of(x => x.Content == contentCache.Object); - var publishedSnapshotAccessor = - Mock.Of(x => x.TryGetPublishedSnapshot(out publishedSnapshot)); - return publishedSnapshotAccessor; + + var publishedContentTypeCacheMock = new Mock(); + publishedContentTypeCacheMock.Setup(x => x.Get(PublishedItemType.Element, ContentKey1)).Returns(test1ContentType); + publishedContentTypeCacheMock.Setup(x => x.Get(PublishedItemType.Element, ContentKey2)).Returns(test2ContentType); + publishedContentTypeCacheMock.Setup(x => x.Get(PublishedItemType.Element, SettingKey1)).Returns(test3ContentType); + publishedContentTypeCacheMock.Setup(x => x.Get(PublishedItemType.Element, SettingKey2)).Returns(test4ContentType); + + return publishedContentTypeCacheMock.Object; } protected IPublishedPropertyType GetPropertyType(TPropertyEditorConfig config) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ConvertersTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ConvertersTests.cs index 031d229a86..55a61bafb7 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ConvertersTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ConvertersTests.cs @@ -14,6 +14,7 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.PublishedCache.Internal; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Cms.Tests.Common.Published; using Umbraco.Cms.Tests.UnitTests.TestHelpers; @@ -50,12 +51,7 @@ public class ConvertersTests var cacheContent = new Dictionary(); cacheMock.Setup(x => x.GetById(It.IsAny())).Returns(id => cacheContent.TryGetValue(id, out var content) ? content : null); - var publishedSnapshotMock = new Mock(); - publishedSnapshotMock.Setup(x => x.Content).Returns(cacheMock.Object); - var publishedSnapshotAccessorMock = new Mock(); - var localPublishedSnapshot = publishedSnapshotMock.Object; - publishedSnapshotAccessorMock.Setup(x => x.TryGetPublishedSnapshot(out localPublishedSnapshot)).Returns(true); - register.AddTransient(f => publishedSnapshotAccessorMock.Object); + register.AddSingleton(f => cacheMock.Object); var registerFactory = composition.CreateServiceProvider(); var converters = @@ -174,10 +170,12 @@ public class ConvertersTests public class SimpleConverter3B : PropertyValueConverterBase { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IPublishedContentCache _publishedContentCache; - public SimpleConverter3B(IPublishedSnapshotAccessor publishedSnapshotAccessor) => - _publishedSnapshotAccessor = publishedSnapshotAccessor; + public SimpleConverter3B(IPublishedContentCache publishedContentCache) + { + _publishedContentCache = publishedContentCache; + } public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias == "Umbraco.Void.2"; @@ -205,9 +203,8 @@ public class ConvertersTests object inter, bool preview) { - var publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); return ((int[])inter).Select(x => - (PublishedSnapshotTestObjects.TestContentModel1)publishedSnapshot.Content + (PublishedSnapshotTestObjects.TestContentModel1)_publishedContentCache .GetById(x)).ToArray(); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/ConvertersTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/ConvertersTests.cs index 2a2f59a32b..7e05897985 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/ConvertersTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/ConvertersTests.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.PublishedCache.Internal; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Extensions; @@ -96,17 +97,11 @@ public class ConvertersTests var cacheContent = new Dictionary(); cacheMock.Setup(x => x.GetById(It.IsAny())) .Returns(id => cacheContent.TryGetValue(id, out var content) ? content : null); - var publishedSnapshotMock = new Mock(); - publishedSnapshotMock.Setup(x => x.Content).Returns(cacheMock.Object); - var publishedSnapshotAccessorMock = new Mock(); - var localPublishedSnapshot = publishedSnapshotMock.Object; - publishedSnapshotAccessorMock.Setup(x => x.TryGetPublishedSnapshot(out localPublishedSnapshot)).Returns(true); - var publishedSnapshotAccessor = publishedSnapshotAccessorMock.Object; - var converters = new PropertyValueConverterCollection(() => new IPropertyValueConverter[] - { - new SimpleConverter2(publishedSnapshotAccessor), - }); + var converters = new PropertyValueConverterCollection(() => + [ + new SimpleConverter2(cacheMock.Object) + ]); var serializer = new SystemTextConfigurationEditorJsonSerializer(); var dataTypeServiceMock = new Mock(); @@ -136,12 +131,12 @@ public class ConvertersTests private class SimpleConverter2 : IPropertyValueConverter { + private readonly IPublishedContentCache _contentCache; private readonly PropertyCacheLevel _cacheLevel; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - public SimpleConverter2(IPublishedSnapshotAccessor publishedSnapshotAccessor, PropertyCacheLevel cacheLevel = PropertyCacheLevel.None) + public SimpleConverter2(IPublishedContentCache contentCache, PropertyCacheLevel cacheLevel = PropertyCacheLevel.None) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; + _contentCache = contentCache; _cacheLevel = cacheLevel; } @@ -172,8 +167,7 @@ public class ConvertersTests object inter, bool preview) { - var publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - return publishedSnapshot.Content.GetById((int)inter); + return _contentCache.GetById((int)inter)!; } public object ConvertIntermediateToXPath( diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PropertyCacheLevelTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PropertyCacheLevelTests.cs index bf786bbc98..ada873977c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PropertyCacheLevelTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PropertyCacheLevelTests.cs @@ -71,43 +71,31 @@ public class PropertyCacheLevelTests // property is not cached, converted cached at Content, exept // /None = not cached at all - [TestCase(PropertyCacheLevel.None, PropertyCacheLevel.None, 2, 0, 0, 0, 0)] - [TestCase(PropertyCacheLevel.None, PropertyCacheLevel.Element, 1, 0, 0, 0, 0)] - [TestCase(PropertyCacheLevel.None, PropertyCacheLevel.Elements, 1, 0, 0, 0, 0)] - [TestCase(PropertyCacheLevel.None, PropertyCacheLevel.Snapshot, 1, 0, 0, 0, 0)] + [TestCase(PropertyCacheLevel.None, PropertyCacheLevel.None, 2, 0, 0)] + [TestCase(PropertyCacheLevel.None, PropertyCacheLevel.Element, 1, 0, 0)] + [TestCase(PropertyCacheLevel.None, PropertyCacheLevel.Elements, 1, 0, 0)] // property is cached at element level, converted cached at // /None = not at all // /Element = in element // /Snapshot = in snapshot // /Elements = in elements - [TestCase(PropertyCacheLevel.Element, PropertyCacheLevel.None, 2, 0, 0, 0, 0)] - [TestCase(PropertyCacheLevel.Element, PropertyCacheLevel.Element, 1, 0, 0, 0, 0)] - [TestCase(PropertyCacheLevel.Element, PropertyCacheLevel.Elements, 1, 1, 0, 1, 0)] - [TestCase(PropertyCacheLevel.Element, PropertyCacheLevel.Snapshot, 1, 0, 1, 0, 1)] + [TestCase(PropertyCacheLevel.Element, PropertyCacheLevel.None, 2, 0, 0)] + [TestCase(PropertyCacheLevel.Element, PropertyCacheLevel.Element, 1, 0, 0)] + [TestCase(PropertyCacheLevel.Element, PropertyCacheLevel.Elements, 1, 1, 1)] // property is cached at elements level, converted cached at Element, exept // /None = not cached at all // /Snapshot = cached in snapshot - [TestCase(PropertyCacheLevel.Elements, PropertyCacheLevel.None, 2, 0, 0, 0, 0)] - [TestCase(PropertyCacheLevel.Elements, PropertyCacheLevel.Element, 1, 0, 0, 0, 0)] - [TestCase(PropertyCacheLevel.Elements, PropertyCacheLevel.Elements, 1, 0, 0, 0, 0)] - [TestCase(PropertyCacheLevel.Elements, PropertyCacheLevel.Snapshot, 1, 0, 1, 0, 1)] - - // property is cached at snapshot level, converted cached at Element, exept - // /None = not cached at all - [TestCase(PropertyCacheLevel.Snapshot, PropertyCacheLevel.None, 2, 0, 0, 0, 0)] - [TestCase(PropertyCacheLevel.Snapshot, PropertyCacheLevel.Element, 1, 0, 0, 0, 0)] - [TestCase(PropertyCacheLevel.Snapshot, PropertyCacheLevel.Elements, 1, 0, 0, 0, 0)] - [TestCase(PropertyCacheLevel.Snapshot, PropertyCacheLevel.Snapshot, 1, 0, 0, 0, 0)] + [TestCase(PropertyCacheLevel.Elements, PropertyCacheLevel.None, 2, 0, 0)] + [TestCase(PropertyCacheLevel.Elements, PropertyCacheLevel.Element, 1, 0, 0)] + [TestCase(PropertyCacheLevel.Elements, PropertyCacheLevel.Elements, 1, 0, 0)] public void CachePublishedSnapshotTest( PropertyCacheLevel referenceCacheLevel, PropertyCacheLevel converterCacheLevel, int interConverts, int elementsCount1, - int snapshotCount1, - int elementsCount2, - int snapshotCount2) + int elementsCount2) { var converter = new CacheConverter1(converterCacheLevel); @@ -129,15 +117,9 @@ public class PropertyCacheLevelTests var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes); var elementsCache = new FastDictionaryAppCache(); - var snapshotCache = new FastDictionaryAppCache(); - var publishedSnapshot = new Mock(); - publishedSnapshot.Setup(x => x.SnapshotCache).Returns(snapshotCache); - publishedSnapshot.Setup(x => x.ElementsCache).Returns(elementsCache); - - var publishedSnapshotAccessor = new Mock(); - var localPublishedSnapshot = publishedSnapshot.Object; - publishedSnapshotAccessor.Setup(x => x.TryGetPublishedSnapshot(out localPublishedSnapshot)).Returns(true); + var cacheManager = new Mock(); + cacheManager.Setup(x => x.ElementsCache).Returns(elementsCache); // pretend we're creating this set as a value for a property // referenceCacheLevel is the cache level for this fictious property @@ -151,33 +133,23 @@ public class PropertyCacheLevelTests }, false, referenceCacheLevel, - publishedSnapshotAccessor.Object); + cacheManager.Object); Assert.AreEqual(1234, set1.Value(Mock.Of(), "prop1")); Assert.AreEqual(1, converter.SourceConverts); Assert.AreEqual(1, converter.InterConverts); Assert.AreEqual(elementsCount1, elementsCache.Count); - Assert.AreEqual(snapshotCount1, snapshotCache.Count); - Assert.AreEqual(1234, set1.Value(Mock.Of(), "prop1")); Assert.AreEqual(1, converter.SourceConverts); Assert.AreEqual(interConverts, converter.InterConverts); Assert.AreEqual(elementsCount2, elementsCache.Count); - Assert.AreEqual(snapshotCount2, snapshotCache.Count); - - var oldSnapshotCache = snapshotCache; - snapshotCache.Clear(); Assert.AreEqual(1234, set1.Value(Mock.Of(), "prop1")); Assert.AreEqual(1, converter.SourceConverts); Assert.AreEqual(elementsCount2, elementsCache.Count); - Assert.AreEqual(snapshotCount2, snapshotCache.Count); - Assert.AreEqual(snapshotCount2, oldSnapshotCache.Count); - - Assert.AreEqual((interConverts == 1 ? 1 : 3) + snapshotCache.Count, converter.InterConverts); var oldElementsCache = elementsCache; elementsCache.Clear(); @@ -187,9 +159,6 @@ public class PropertyCacheLevelTests Assert.AreEqual(elementsCount2, elementsCache.Count); Assert.AreEqual(elementsCount2, oldElementsCache.Count); - Assert.AreEqual(snapshotCount2, snapshotCache.Count); - - Assert.AreEqual((interConverts == 1 ? 1 : 4) + snapshotCache.Count + elementsCache.Count, converter.InterConverts); } [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PropertyCacheVarianceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PropertyCacheVarianceTests.cs index 72a68b443c..78357027ef 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PropertyCacheVarianceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PropertyCacheVarianceTests.cs @@ -1,381 +1,382 @@ -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Property = Umbraco.Cms.Infrastructure.PublishedCache.Property; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Published; - -[TestFixture] -public class PropertyCacheVarianceTests -{ - // This class tests various permutations of property value calculation across variance types and cache levels. - // - // Properties contain different "value levels", all of which are cached: - // 1. The source value => the "raw" value from the client side editor (it can be different, but it's easiest to think of it like that). - // 2. The intermediate value => a "temporary" value that is used to calculate the various "final" values. - // 3. The object value => the "final" object value that is exposed in an IPublishedElement output. - // 4. The XPath value => a legacy "final" value, don't think too hard on it. - // 3. The delivery API object value => the "final" object value that is exposed in the Delivery API. - // - // Property values are cached based on a few rules: - // 1. The property type variation and the parent content type variation determines how the intermediate value is cached. - // The effective property variation is a product of both variations, meaning the property type and the content type - // variations are combined in an OR. - // The rules are as follows: - // - ContentVariation.Nothing => the intermediate value is calculated once and reused across all variants (cultures and segments). - // - ContentVariation.Culture => the intermediate value is calculated per culture and reused across all segments. - // - ContentVariation.Segment => the intermediate value is calculated per segment and reused across all cultures. - // - ContentVariation.CultureAndSegment => the intermediate value is calculated for all invoked culture and segment combinations. - // 2. The property type cache level (which is usually derived from the property value converter). - // - PropertyCacheLevel.Element => the final values are cached until the parent content item is updated. - // - PropertyCacheLevel.Elements => the final values are cached until the _any_ content item is updated. - // - PropertyCacheLevel.Snapshot => the final values are cached for the duration of the active cache snapshot (i.e. until the end of the current request). - // - PropertyCacheLevel.None => the final values are never cached and will be re-calculated each time they're requested. - - // ### Invariant content type + invariant property type ### - [TestCase( - ContentVariation.Nothing, - ContentVariation.Nothing, - PropertyCacheLevel.Element, - // no variation => the intermediate value is calculated only once - // cache level => the final value is calculated only once - "da-DK:segment1 (da-DK:segment1)", - "da-DK:segment1 (da-DK:segment1)", - "da-DK:segment1 (da-DK:segment1)", - "da-DK:segment1 (da-DK:segment1)")] - [TestCase( - ContentVariation.Nothing, - ContentVariation.Nothing, - PropertyCacheLevel.Elements, - "da-DK:segment1 (da-DK:segment1)", - "da-DK:segment1 (da-DK:segment1)", - "da-DK:segment1 (da-DK:segment1)", - "da-DK:segment1 (da-DK:segment1)")] - [TestCase( - ContentVariation.Nothing, - ContentVariation.Nothing, - PropertyCacheLevel.Snapshot, - "da-DK:segment1 (da-DK:segment1)", - "da-DK:segment1 (da-DK:segment1)", - "da-DK:segment1 (da-DK:segment1)", - "da-DK:segment1 (da-DK:segment1)")] - [TestCase( - ContentVariation.Nothing, - ContentVariation.Nothing, - PropertyCacheLevel.None, - // no variation => the intermediate value is calculated once - // no cache => the final value is calculated for each request (reflects both changes in culture and segments) - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (da-DK:segment1)", - "en-US:segment2 (da-DK:segment1)", - "da-DK:segment2 (da-DK:segment1)")] - // ### Culture variant content type + invariant property type ### - [TestCase( - ContentVariation.Culture, - ContentVariation.Nothing, - PropertyCacheLevel.Element, - // culture variation => the intermediate value is calculated per culture (ignores segment changes until a culture changes) - // cache level => the final value is calculated only once per culture (ignores segment changes until a culture changes) - // NOTE: in this test, culture changes before segment, so the updated segment is never reflected here - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment1 (en-US:segment1)", - "da-DK:segment1 (da-DK:segment1)")] - [TestCase( - ContentVariation.Culture, - ContentVariation.Nothing, - PropertyCacheLevel.Elements, - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment1 (en-US:segment1)", - "da-DK:segment1 (da-DK:segment1)")] - [TestCase( - ContentVariation.Culture, - ContentVariation.Nothing, - PropertyCacheLevel.Snapshot, - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment1 (en-US:segment1)", - "da-DK:segment1 (da-DK:segment1)")] - [TestCase( - ContentVariation.Culture, - ContentVariation.Nothing, - PropertyCacheLevel.None, - // culture variation => the intermediate value is calculated per culture (ignores segment changes until a culture changes) - // no cache => the final value is calculated for each request (reflects both changes in culture and segments) - // NOTE: in this test, culture changes before segment, so the updated segment is never reflected in the intermediate value here - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment2 (en-US:segment1)", - "da-DK:segment2 (da-DK:segment1)")] - // NOTE: As the tests above show, cache levels Element, Elements and Snapshot all yield the same values in this - // test, because we are efficiently executing the test in a snapshot. From here on out we're only building - // test cases for Element and None. - // ### Segment variant content type + invariant property type ### - [TestCase( - ContentVariation.Segment, - ContentVariation.Nothing, - PropertyCacheLevel.Element, - // segment variation => the intermediate value is calculated per segment (ignores culture changes until a segment changes) - // cache level => the final value is calculated only once per segment (ignores culture changes until a segment changes) - "da-DK:segment1 (da-DK:segment1)", - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment2 (en-US:segment2)", - "en-US:segment2 (en-US:segment2)")] - [TestCase( - ContentVariation.Segment, - ContentVariation.Nothing, - PropertyCacheLevel.None, - // segment variation => the intermediate value is calculated per segment (ignores culture changes until a segment changes) - // no cache => the final value is calculated for each request (reflects both changes in culture and segments) - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (da-DK:segment1)", - "en-US:segment2 (en-US:segment2)", - "da-DK:segment2 (en-US:segment2)")] - // ### Culture and segment variant content type + invariant property type ### - [TestCase( - ContentVariation.CultureAndSegment, - ContentVariation.Nothing, - PropertyCacheLevel.Element, - // culture and segment variation => the intermediate value is calculated per culture and segment - // cache level => the final value is calculated only once per culture and segment (efficiently on every request in this test) - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment2 (en-US:segment2)", - "da-DK:segment2 (da-DK:segment2)")] - [TestCase( - ContentVariation.CultureAndSegment, - ContentVariation.Nothing, - PropertyCacheLevel.None, - // culture and segment variation => the intermediate value is calculated per culture and segment - // no cache => the final value is calculated for each request - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment2 (en-US:segment2)", - "da-DK:segment2 (da-DK:segment2)")] - // ### Invariant content type + culture variant property type ### - [TestCase( - ContentVariation.Nothing, - ContentVariation.Culture, - PropertyCacheLevel.Element, - // same behaviour as culture variation on content type + no variation on property type, see comments above - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment1 (en-US:segment1)", - "da-DK:segment1 (da-DK:segment1)")] - [TestCase( - ContentVariation.Nothing, - ContentVariation.Culture, - PropertyCacheLevel.None, - // same behaviour as culture variation on content type + no variation on property type, see comments above - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment2 (en-US:segment1)", - "da-DK:segment2 (da-DK:segment1)")] - // ### Invariant content type + segment variant property type ### - [TestCase( - ContentVariation.Nothing, - ContentVariation.Segment, - PropertyCacheLevel.Element, - // same behaviour as segment variation on content type + no variation on property type, see comments above - "da-DK:segment1 (da-DK:segment1)", - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment2 (en-US:segment2)", - "en-US:segment2 (en-US:segment2)")] - [TestCase( - ContentVariation.Nothing, - ContentVariation.Segment, - PropertyCacheLevel.None, - // same behaviour as segment variation on content type + no variation on property type, see comments above - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (da-DK:segment1)", - "en-US:segment2 (en-US:segment2)", - "da-DK:segment2 (en-US:segment2)")] - // ### Invariant content type + culture and segment variant property type ### - [TestCase( - ContentVariation.Nothing, - ContentVariation.CultureAndSegment, - PropertyCacheLevel.Element, - // same behaviour as culture and segment variation on content type + no variation on property type, see comments above - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment2 (en-US:segment2)", - "da-DK:segment2 (da-DK:segment2)")] - [TestCase( - ContentVariation.Nothing, - ContentVariation.CultureAndSegment, - PropertyCacheLevel.None, - // same behaviour as culture and segment variation on content type + no variation on property type, see comments above - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment2 (en-US:segment2)", - "da-DK:segment2 (da-DK:segment2)")] - // ### Culture variant content type + segment variant property type ### - [TestCase( - ContentVariation.Culture, - ContentVariation.Segment, - PropertyCacheLevel.Element, - // same behaviour as culture and segment variation on content type + no variation on property type, see comments above - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment2 (en-US:segment2)", - "da-DK:segment2 (da-DK:segment2)")] - [TestCase( - ContentVariation.Culture, - ContentVariation.Segment, - PropertyCacheLevel.None, - // same behaviour as culture and segment variation on content type + no variation on property type, see comments above - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment2 (en-US:segment2)", - "da-DK:segment2 (da-DK:segment2)")] - public void ContentType_PropertyType_Variation_Cache_Values( - ContentVariation contentTypeVariation, - ContentVariation propertyTypeVariation, - PropertyCacheLevel propertyCacheLevel, - string expectedValue1DaDkSegment1, - string expectedValue2EnUsSegment1, - string expectedValue3EnUsSegment2, - string expectedValue4DaDkSegment2) - { - var variationContextCulture = "da-DK"; - var variationContextSegment = "segment1"; - var property = CreateProperty( - contentTypeVariation, - propertyTypeVariation, - propertyCacheLevel, - () => variationContextCulture, - () => variationContextSegment); - - Assert.AreEqual(expectedValue1DaDkSegment1, property.GetValue()); - - variationContextCulture = "en-US"; - Assert.AreEqual(expectedValue2EnUsSegment1, property.GetValue()); - - variationContextSegment = "segment2"; - Assert.AreEqual(expectedValue3EnUsSegment2, property.GetValue()); - - variationContextCulture = "da-DK"; - Assert.AreEqual(expectedValue4DaDkSegment2, property.GetValue()); - } - - [TestCase( - ContentVariation.Culture, - ContentVariation.Nothing, - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment1 (en-US:segment1)", - "da-DK:segment1 (da-DK:segment1)")] - [TestCase( - ContentVariation.Segment, - ContentVariation.Nothing, - "da-DK:segment1 (da-DK:segment1)", - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment2 (en-US:segment2)", - "en-US:segment2 (en-US:segment2)")] - [TestCase( - ContentVariation.Culture, - ContentVariation.Segment, - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment2 (en-US:segment2)", - "da-DK:segment2 (da-DK:segment2)")] - [TestCase( - ContentVariation.CultureAndSegment, - ContentVariation.Nothing, - "da-DK:segment1 (da-DK:segment1)", - "en-US:segment1 (en-US:segment1)", - "en-US:segment2 (en-US:segment2)", - "da-DK:segment2 (da-DK:segment2)")] - public void ContentType_PropertyType_Variation_Are_Interchangeable( - ContentVariation variation1, - ContentVariation variation2, - string expectedValue1DaDkSegment1, - string expectedValue2EnUsSegment1, - string expectedValue3EnUsSegment2, - string expectedValue4DaDkSegment2) - { - var scenarios = new[] - { - new { ContentTypeVariation = variation1, PropertyTypeVariation = variation2 }, - new { ContentTypeVariation = variation2, PropertyTypeVariation = variation1 } - }; - - foreach (var scenario in scenarios) - { - var variationContextCulture = "da-DK"; - var variationContextSegment = "segment1"; - var property = CreateProperty( - scenario.ContentTypeVariation, - scenario.PropertyTypeVariation, - PropertyCacheLevel.Element, - () => variationContextCulture, - () => variationContextSegment); - - Assert.AreEqual(expectedValue1DaDkSegment1, property.GetValue()); - - variationContextCulture = "en-US"; - Assert.AreEqual(expectedValue2EnUsSegment1, property.GetValue()); - - variationContextSegment = "segment2"; - Assert.AreEqual(expectedValue3EnUsSegment2, property.GetValue()); - - variationContextCulture = "da-DK"; - Assert.AreEqual(expectedValue4DaDkSegment2, property.GetValue()); - } - } - - /// - /// Creates a new property with a mocked publishedSnapshotAccessor that uses a VariationContext that reads culture and segment information from the passed in functions. - /// - private Property CreateProperty(ContentVariation contentTypeVariation, ContentVariation propertyTypeVariation, PropertyCacheLevel propertyTypeCacheLevel, Func getCulture, Func getSegment) - { - var contentType = new Mock(); - contentType.SetupGet(c => c.PropertyTypes).Returns(Array.Empty()); - contentType.SetupGet(c => c.Variations).Returns(contentTypeVariation); - - var contentNode = new ContentNode(123, Guid.NewGuid(), contentType.Object, 1, string.Empty, 1, 1, DateTime.Now, 1); - var contentData = new ContentData("bla", "bla", 1, DateTime.Now, 1, 1, true, new Dictionary(), null); - - var elementCache = new FastDictionaryAppCache(); - var snapshotCache = new FastDictionaryAppCache(); - var publishedSnapshotMock = new Mock(); - publishedSnapshotMock.SetupGet(p => p.ElementsCache).Returns(elementCache); - publishedSnapshotMock.SetupGet(p => p.SnapshotCache).Returns(snapshotCache); - - var publishedSnapshot = publishedSnapshotMock.Object; - var publishedSnapshotAccessor = new Mock(); - publishedSnapshotAccessor.Setup(p => p.TryGetPublishedSnapshot(out publishedSnapshot)).Returns(true); - - var variationContextAccessorMock = new Mock(); - variationContextAccessorMock - .SetupGet(mock => mock.VariationContext) - .Returns(() => new VariationContext(getCulture(), getSegment())); - - var content = new PublishedContent( - contentNode, - contentData, - publishedSnapshotAccessor.Object, - variationContextAccessorMock.Object, - Mock.Of()); - - var propertyType = new Mock(); - propertyType.SetupGet(p => p.CacheLevel).Returns(propertyTypeCacheLevel); - propertyType.SetupGet(p => p.DeliveryApiCacheLevel).Returns(propertyTypeCacheLevel); - propertyType.SetupGet(p => p.Variations).Returns(propertyTypeVariation); - propertyType - .Setup(p => p.ConvertSourceToInter(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(() => $"{getCulture()}:{getSegment()}"); - propertyType - .Setup(p => p.ConvertInterToObject(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((IPublishedElement _, PropertyCacheLevel _, object? inter, bool _) => $"{getCulture()}:{getSegment()} ({inter})" ); - - return new Property(propertyType.Object, content, publishedSnapshotAccessor.Object); - } -} +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Cache; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Core.PropertyEditors; +// using Umbraco.Cms.Core.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +// using Property = Umbraco.Cms.Infrastructure.PublishedCache.Property; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Published; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class PropertyCacheVarianceTests +// { +// // This class tests various permutations of property value calculation across variance types and cache levels. +// // +// // Properties contain different "value levels", all of which are cached: +// // 1. The source value => the "raw" value from the client side editor (it can be different, but it's easiest to think of it like that). +// // 2. The intermediate value => a "temporary" value that is used to calculate the various "final" values. +// // 3. The object value => the "final" object value that is exposed in an IPublishedElement output. +// // 4. The XPath value => a legacy "final" value, don't think too hard on it. +// // 3. The delivery API object value => the "final" object value that is exposed in the Delivery API. +// // +// // Property values are cached based on a few rules: +// // 1. The property type variation and the parent content type variation determines how the intermediate value is cached. +// // The effective property variation is a product of both variations, meaning the property type and the content type +// // variations are combined in an OR. +// // The rules are as follows: +// // - ContentVariation.Nothing => the intermediate value is calculated once and reused across all variants (cultures and segments). +// // - ContentVariation.Culture => the intermediate value is calculated per culture and reused across all segments. +// // - ContentVariation.Segment => the intermediate value is calculated per segment and reused across all cultures. +// // - ContentVariation.CultureAndSegment => the intermediate value is calculated for all invoked culture and segment combinations. +// // 2. The property type cache level (which is usually derived from the property value converter). +// // - PropertyCacheLevel.Element => the final values are cached until the parent content item is updated. +// // - PropertyCacheLevel.Elements => the final values are cached until the _any_ content item is updated. +// // - PropertyCacheLevel.Snapshot => the final values are cached for the duration of the active cache snapshot (i.e. until the end of the current request). +// // - PropertyCacheLevel.None => the final values are never cached and will be re-calculated each time they're requested. +// +// // ### Invariant content type + invariant property type ### +// [TestCase( +// ContentVariation.Nothing, +// ContentVariation.Nothing, +// PropertyCacheLevel.Element, +// // no variation => the intermediate value is calculated only once +// // cache level => the final value is calculated only once +// "da-DK:segment1 (da-DK:segment1)", +// "da-DK:segment1 (da-DK:segment1)", +// "da-DK:segment1 (da-DK:segment1)", +// "da-DK:segment1 (da-DK:segment1)")] +// [TestCase( +// ContentVariation.Nothing, +// ContentVariation.Nothing, +// PropertyCacheLevel.Elements, +// "da-DK:segment1 (da-DK:segment1)", +// "da-DK:segment1 (da-DK:segment1)", +// "da-DK:segment1 (da-DK:segment1)", +// "da-DK:segment1 (da-DK:segment1)")] +// [TestCase( +// ContentVariation.Nothing, +// ContentVariation.Nothing, +// PropertyCacheLevel.Snapshot, +// "da-DK:segment1 (da-DK:segment1)", +// "da-DK:segment1 (da-DK:segment1)", +// "da-DK:segment1 (da-DK:segment1)", +// "da-DK:segment1 (da-DK:segment1)")] +// [TestCase( +// ContentVariation.Nothing, +// ContentVariation.Nothing, +// PropertyCacheLevel.None, +// // no variation => the intermediate value is calculated once +// // no cache => the final value is calculated for each request (reflects both changes in culture and segments) +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (da-DK:segment1)", +// "en-US:segment2 (da-DK:segment1)", +// "da-DK:segment2 (da-DK:segment1)")] +// // ### Culture variant content type + invariant property type ### +// [TestCase( +// ContentVariation.Culture, +// ContentVariation.Nothing, +// PropertyCacheLevel.Element, +// // culture variation => the intermediate value is calculated per culture (ignores segment changes until a culture changes) +// // cache level => the final value is calculated only once per culture (ignores segment changes until a culture changes) +// // NOTE: in this test, culture changes before segment, so the updated segment is never reflected here +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "da-DK:segment1 (da-DK:segment1)")] +// [TestCase( +// ContentVariation.Culture, +// ContentVariation.Nothing, +// PropertyCacheLevel.Elements, +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "da-DK:segment1 (da-DK:segment1)")] +// [TestCase( +// ContentVariation.Culture, +// ContentVariation.Nothing, +// PropertyCacheLevel.Snapshot, +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "da-DK:segment1 (da-DK:segment1)")] +// [TestCase( +// ContentVariation.Culture, +// ContentVariation.Nothing, +// PropertyCacheLevel.None, +// // culture variation => the intermediate value is calculated per culture (ignores segment changes until a culture changes) +// // no cache => the final value is calculated for each request (reflects both changes in culture and segments) +// // NOTE: in this test, culture changes before segment, so the updated segment is never reflected in the intermediate value here +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment2 (en-US:segment1)", +// "da-DK:segment2 (da-DK:segment1)")] +// // NOTE: As the tests above show, cache levels Element, Elements and Snapshot all yield the same values in this +// // test, because we are efficiently executing the test in a snapshot. From here on out we're only building +// // test cases for Element and None. +// // ### Segment variant content type + invariant property type ### +// [TestCase( +// ContentVariation.Segment, +// ContentVariation.Nothing, +// PropertyCacheLevel.Element, +// // segment variation => the intermediate value is calculated per segment (ignores culture changes until a segment changes) +// // cache level => the final value is calculated only once per segment (ignores culture changes until a segment changes) +// "da-DK:segment1 (da-DK:segment1)", +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "en-US:segment2 (en-US:segment2)")] +// [TestCase( +// ContentVariation.Segment, +// ContentVariation.Nothing, +// PropertyCacheLevel.None, +// // segment variation => the intermediate value is calculated per segment (ignores culture changes until a segment changes) +// // no cache => the final value is calculated for each request (reflects both changes in culture and segments) +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (da-DK:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "da-DK:segment2 (en-US:segment2)")] +// // ### Culture and segment variant content type + invariant property type ### +// [TestCase( +// ContentVariation.CultureAndSegment, +// ContentVariation.Nothing, +// PropertyCacheLevel.Element, +// // culture and segment variation => the intermediate value is calculated per culture and segment +// // cache level => the final value is calculated only once per culture and segment (efficiently on every request in this test) +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "da-DK:segment2 (da-DK:segment2)")] +// [TestCase( +// ContentVariation.CultureAndSegment, +// ContentVariation.Nothing, +// PropertyCacheLevel.None, +// // culture and segment variation => the intermediate value is calculated per culture and segment +// // no cache => the final value is calculated for each request +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "da-DK:segment2 (da-DK:segment2)")] +// // ### Invariant content type + culture variant property type ### +// [TestCase( +// ContentVariation.Nothing, +// ContentVariation.Culture, +// PropertyCacheLevel.Element, +// // same behaviour as culture variation on content type + no variation on property type, see comments above +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "da-DK:segment1 (da-DK:segment1)")] +// [TestCase( +// ContentVariation.Nothing, +// ContentVariation.Culture, +// PropertyCacheLevel.None, +// // same behaviour as culture variation on content type + no variation on property type, see comments above +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment2 (en-US:segment1)", +// "da-DK:segment2 (da-DK:segment1)")] +// // ### Invariant content type + segment variant property type ### +// [TestCase( +// ContentVariation.Nothing, +// ContentVariation.Segment, +// PropertyCacheLevel.Element, +// // same behaviour as segment variation on content type + no variation on property type, see comments above +// "da-DK:segment1 (da-DK:segment1)", +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "en-US:segment2 (en-US:segment2)")] +// [TestCase( +// ContentVariation.Nothing, +// ContentVariation.Segment, +// PropertyCacheLevel.None, +// // same behaviour as segment variation on content type + no variation on property type, see comments above +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (da-DK:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "da-DK:segment2 (en-US:segment2)")] +// // ### Invariant content type + culture and segment variant property type ### +// [TestCase( +// ContentVariation.Nothing, +// ContentVariation.CultureAndSegment, +// PropertyCacheLevel.Element, +// // same behaviour as culture and segment variation on content type + no variation on property type, see comments above +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "da-DK:segment2 (da-DK:segment2)")] +// [TestCase( +// ContentVariation.Nothing, +// ContentVariation.CultureAndSegment, +// PropertyCacheLevel.None, +// // same behaviour as culture and segment variation on content type + no variation on property type, see comments above +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "da-DK:segment2 (da-DK:segment2)")] +// // ### Culture variant content type + segment variant property type ### +// [TestCase( +// ContentVariation.Culture, +// ContentVariation.Segment, +// PropertyCacheLevel.Element, +// // same behaviour as culture and segment variation on content type + no variation on property type, see comments above +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "da-DK:segment2 (da-DK:segment2)")] +// [TestCase( +// ContentVariation.Culture, +// ContentVariation.Segment, +// PropertyCacheLevel.None, +// // same behaviour as culture and segment variation on content type + no variation on property type, see comments above +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "da-DK:segment2 (da-DK:segment2)")] +// public void ContentType_PropertyType_Variation_Cache_Values( +// ContentVariation contentTypeVariation, +// ContentVariation propertyTypeVariation, +// PropertyCacheLevel propertyCacheLevel, +// string expectedValue1DaDkSegment1, +// string expectedValue2EnUsSegment1, +// string expectedValue3EnUsSegment2, +// string expectedValue4DaDkSegment2) +// { +// var variationContextCulture = "da-DK"; +// var variationContextSegment = "segment1"; +// var property = CreateProperty( +// contentTypeVariation, +// propertyTypeVariation, +// propertyCacheLevel, +// () => variationContextCulture, +// () => variationContextSegment); +// +// Assert.AreEqual(expectedValue1DaDkSegment1, property.GetValue()); +// +// variationContextCulture = "en-US"; +// Assert.AreEqual(expectedValue2EnUsSegment1, property.GetValue()); +// +// variationContextSegment = "segment2"; +// Assert.AreEqual(expectedValue3EnUsSegment2, property.GetValue()); +// +// variationContextCulture = "da-DK"; +// Assert.AreEqual(expectedValue4DaDkSegment2, property.GetValue()); +// } +// +// [TestCase( +// ContentVariation.Culture, +// ContentVariation.Nothing, +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "da-DK:segment1 (da-DK:segment1)")] +// [TestCase( +// ContentVariation.Segment, +// ContentVariation.Nothing, +// "da-DK:segment1 (da-DK:segment1)", +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "en-US:segment2 (en-US:segment2)")] +// [TestCase( +// ContentVariation.Culture, +// ContentVariation.Segment, +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "da-DK:segment2 (da-DK:segment2)")] +// [TestCase( +// ContentVariation.CultureAndSegment, +// ContentVariation.Nothing, +// "da-DK:segment1 (da-DK:segment1)", +// "en-US:segment1 (en-US:segment1)", +// "en-US:segment2 (en-US:segment2)", +// "da-DK:segment2 (da-DK:segment2)")] +// public void ContentType_PropertyType_Variation_Are_Interchangeable( +// ContentVariation variation1, +// ContentVariation variation2, +// string expectedValue1DaDkSegment1, +// string expectedValue2EnUsSegment1, +// string expectedValue3EnUsSegment2, +// string expectedValue4DaDkSegment2) +// { +// var scenarios = new[] +// { +// new { ContentTypeVariation = variation1, PropertyTypeVariation = variation2 }, +// new { ContentTypeVariation = variation2, PropertyTypeVariation = variation1 } +// }; +// +// foreach (var scenario in scenarios) +// { +// var variationContextCulture = "da-DK"; +// var variationContextSegment = "segment1"; +// var property = CreateProperty( +// scenario.ContentTypeVariation, +// scenario.PropertyTypeVariation, +// PropertyCacheLevel.Element, +// () => variationContextCulture, +// () => variationContextSegment); +// +// Assert.AreEqual(expectedValue1DaDkSegment1, property.GetValue()); +// +// variationContextCulture = "en-US"; +// Assert.AreEqual(expectedValue2EnUsSegment1, property.GetValue()); +// +// variationContextSegment = "segment2"; +// Assert.AreEqual(expectedValue3EnUsSegment2, property.GetValue()); +// +// variationContextCulture = "da-DK"; +// Assert.AreEqual(expectedValue4DaDkSegment2, property.GetValue()); +// } +// } +// +// /// +// /// Creates a new property with a mocked publishedSnapshotAccessor that uses a VariationContext that reads culture and segment information from the passed in functions. +// /// +// private Property CreateProperty(ContentVariation contentTypeVariation, ContentVariation propertyTypeVariation, PropertyCacheLevel propertyTypeCacheLevel, Func getCulture, Func getSegment) +// { +// var contentType = new Mock(); +// contentType.SetupGet(c => c.PropertyTypes).Returns(Array.Empty()); +// contentType.SetupGet(c => c.Variations).Returns(contentTypeVariation); +// +// var contentNode = new ContentNode(123, Guid.NewGuid(), contentType.Object, 1, string.Empty, 1, 1, DateTime.Now, 1); +// var contentData = new ContentData("bla", "bla", 1, DateTime.Now, 1, 1, true, new Dictionary(), null); +// +// var elementCache = new FastDictionaryAppCache(); +// var snapshotCache = new FastDictionaryAppCache(); +// var publishedSnapshotMock = new Mock(); +// publishedSnapshotMock.SetupGet(p => p.ElementsCache).Returns(elementCache); +// publishedSnapshotMock.SetupGet(p => p.SnapshotCache).Returns(snapshotCache); +// +// var publishedSnapshot = publishedSnapshotMock.Object; +// var publishedSnapshotAccessor = new Mock(); +// publishedSnapshotAccessor.Setup(p => p.TryGetPublishedSnapshot(out publishedSnapshot)).Returns(true); +// +// var variationContextAccessorMock = new Mock(); +// variationContextAccessorMock +// .SetupGet(mock => mock.VariationContext) +// .Returns(() => new VariationContext(getCulture(), getSegment())); +// +// var content = new PublishedContent( +// contentNode, +// contentData, +// publishedSnapshotAccessor.Object, +// variationContextAccessorMock.Object, +// Mock.Of()); +// +// var propertyType = new Mock(); +// propertyType.SetupGet(p => p.CacheLevel).Returns(propertyTypeCacheLevel); +// propertyType.SetupGet(p => p.DeliveryApiCacheLevel).Returns(propertyTypeCacheLevel); +// propertyType.SetupGet(p => p.Variations).Returns(propertyTypeVariation); +// propertyType +// .Setup(p => p.ConvertSourceToInter(It.IsAny(), It.IsAny(), It.IsAny())) +// .Returns(() => $"{getCulture()}:{getSegment()}"); +// propertyType +// .Setup(p => p.ConvertInterToObject(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) +// .Returns((IPublishedElement _, PropertyCacheLevel _, object? inter, bool _) => $"{getCulture()}:{getSegment()} ({inter})" ); +// +// return new Property(propertyType.Object, content, publishedSnapshotAccessor.Object); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PublishedContentVarianceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PublishedContentVarianceTests.cs index a4a15b8f22..e603d682c1 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PublishedContentVarianceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PublishedContentVarianceTests.cs @@ -1,174 +1,175 @@ -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Published; - -[TestFixture] -public class PublishedContentVarianceTests -{ - private const string PropertyTypeAlias = "theProperty"; - private const string DaCulture = "da-DK"; - private const string EnCulture = "en-US"; - private const string Segment1 = "segment1"; - private const string Segment2 = "segment2"; - - [Test] - public void No_Content_Variation_Can_Get_Invariant_Property() - { - var content = CreatePublishedContent(ContentVariation.Nothing, ContentVariation.Nothing); - var value = GetPropertyValue(content); - Assert.AreEqual("Invariant property value", value); - } - - [TestCase(DaCulture)] - [TestCase(EnCulture)] - [TestCase("")] - public void Content_Culture_Variation_Can_Get_Invariant_Property(string culture) - { - var content = CreatePublishedContent(ContentVariation.Culture, ContentVariation.Nothing, variationContextCulture: culture); - var value = GetPropertyValue(content); - Assert.AreEqual("Invariant property value", value); - } - - [TestCase(Segment1)] - [TestCase(Segment2)] - [TestCase("")] - public void Content_Segment_Variation_Can_Get_Invariant_Property(string segment) - { - var content = CreatePublishedContent(ContentVariation.Culture, ContentVariation.Nothing, variationContextSegment: segment); - var value = GetPropertyValue(content); - Assert.AreEqual("Invariant property value", value); - } - - [TestCase(DaCulture, "DaDk property value")] - [TestCase(EnCulture, "EnUs property value")] - public void Content_Culture_Variation_Can_Get_Culture_Variant_Property(string culture, string expectedValue) - { - var content = CreatePublishedContent(ContentVariation.Culture, ContentVariation.Culture, variationContextCulture: culture); - var value = GetPropertyValue(content); - Assert.AreEqual(expectedValue, value); - } - - [TestCase(Segment1, "Segment1 property value")] - [TestCase(Segment2, "Segment2 property value")] - public void Content_Segment_Variation_Can_Get_Segment_Variant_Property(string segment, string expectedValue) - { - var content = CreatePublishedContent(ContentVariation.Segment, ContentVariation.Segment, variationContextSegment: segment); - var value = GetPropertyValue(content); - Assert.AreEqual(expectedValue, value); - } - - [TestCase(DaCulture, Segment1, "DaDk Segment1 property value")] - [TestCase(DaCulture, Segment2, "DaDk Segment2 property value")] - [TestCase(EnCulture, Segment1, "EnUs Segment1 property value")] - [TestCase(EnCulture, Segment2, "EnUs Segment2 property value")] - public void Content_Culture_And_Segment_Variation_Can_Get_Culture_And_Segment_Variant_Property(string culture, string segment, string expectedValue) - { - var content = CreatePublishedContent(ContentVariation.CultureAndSegment, ContentVariation.CultureAndSegment, variationContextCulture: culture, variationContextSegment: segment); - var value = GetPropertyValue(content); - Assert.AreEqual(expectedValue, value); - } - - [TestCase(DaCulture, Segment1, "DaDk property value")] - [TestCase(DaCulture, Segment2, "DaDk property value")] - [TestCase(EnCulture, Segment1, "EnUs property value")] - [TestCase(EnCulture, Segment2, "EnUs property value")] - public void Content_Culture_And_Segment_Variation_Can_Get_Culture_Variant_Property(string culture, string segment, string expectedValue) - { - var content = CreatePublishedContent(ContentVariation.CultureAndSegment, ContentVariation.Culture, variationContextCulture: culture, variationContextSegment: segment); - var value = GetPropertyValue(content); - Assert.AreEqual(expectedValue, value); - } - - [TestCase(DaCulture, Segment1, "Segment1 property value")] - [TestCase(DaCulture, Segment2, "Segment2 property value")] - [TestCase(EnCulture, Segment1, "Segment1 property value")] - [TestCase(EnCulture, Segment2, "Segment2 property value")] - public void Content_Culture_And_Segment_Variation_Can_Get_Segment_Variant_Property(string culture, string segment, string expectedValue) - { - var content = CreatePublishedContent(ContentVariation.CultureAndSegment, ContentVariation.Segment, variationContextCulture: culture, variationContextSegment: segment); - var value = GetPropertyValue(content); - Assert.AreEqual(expectedValue, value); - } - - private object? GetPropertyValue(IPublishedContent content) => content.GetProperty(PropertyTypeAlias)!.GetValue(); - - private IPublishedContent CreatePublishedContent(ContentVariation contentTypeVariation, ContentVariation propertyTypeVariation, string? variationContextCulture = null, string? variationContextSegment = null) - { - var propertyType = new Mock(); - propertyType.SetupGet(p => p.Alias).Returns(PropertyTypeAlias); - propertyType.SetupGet(p => p.CacheLevel).Returns(PropertyCacheLevel.None); - propertyType.SetupGet(p => p.DeliveryApiCacheLevel).Returns(PropertyCacheLevel.None); - propertyType.SetupGet(p => p.Variations).Returns(propertyTypeVariation); - propertyType - .Setup(p => p.ConvertSourceToInter(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((IPublishedElement _, object? source, bool _) => source); - propertyType - .Setup(p => p.ConvertInterToObject(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((IPublishedElement _, PropertyCacheLevel _, object? inter, bool _) => inter); - - var contentType = new Mock(); - contentType.SetupGet(c => c.PropertyTypes).Returns(new[] { propertyType.Object }); - contentType.SetupGet(c => c.Variations).Returns(contentTypeVariation); - - var propertyData = new List(); - - switch (propertyTypeVariation) - { - case ContentVariation.Culture: - propertyData.Add(CreatePropertyData("EnUs property value", culture: EnCulture)); - propertyData.Add(CreatePropertyData("DaDk property value", culture: DaCulture)); - break; - case ContentVariation.Segment: - propertyData.Add(CreatePropertyData("Segment1 property value", segment: Segment1)); - propertyData.Add(CreatePropertyData("Segment2 property value", segment: Segment2)); - break; - case ContentVariation.CultureAndSegment: - propertyData.Add(CreatePropertyData("EnUs Segment1 property value", culture: EnCulture, segment: Segment1)); - propertyData.Add(CreatePropertyData("EnUs Segment2 property value", culture: EnCulture, segment: Segment2)); - propertyData.Add(CreatePropertyData("DaDk Segment1 property value", culture: DaCulture, segment: Segment1)); - propertyData.Add(CreatePropertyData("DaDk Segment2 property value", culture: DaCulture, segment: Segment2)); - break; - case ContentVariation.Nothing: - propertyData.Add(CreatePropertyData("Invariant property value")); - break; - } - - var properties = new Dictionary { { PropertyTypeAlias, propertyData.ToArray() } }; - - var contentNode = new ContentNode(123, Guid.NewGuid(), contentType.Object, 1, string.Empty, 1, 1, DateTime.Now, 1); - var contentData = new ContentData("bla", "bla", 1, DateTime.Now, 1, 1, true, properties, null); - - var elementCache = new FastDictionaryAppCache(); - var snapshotCache = new FastDictionaryAppCache(); - var publishedSnapshotMock = new Mock(); - publishedSnapshotMock.SetupGet(p => p.ElementsCache).Returns(elementCache); - publishedSnapshotMock.SetupGet(p => p.SnapshotCache).Returns(snapshotCache); - - var publishedSnapshot = publishedSnapshotMock.Object; - var publishedSnapshotAccessor = new Mock(); - publishedSnapshotAccessor.Setup(p => p.TryGetPublishedSnapshot(out publishedSnapshot)).Returns(true); - - var variationContextAccessorMock = new Mock(); - variationContextAccessorMock - .SetupGet(mock => mock.VariationContext) - .Returns(() => new VariationContext(variationContextCulture, variationContextSegment)); - - return new PublishedContent( - contentNode, - contentData, - publishedSnapshotAccessor.Object, - variationContextAccessorMock.Object, - Mock.Of()); - - PropertyData CreatePropertyData(string value, string? culture = null, string? segment = null) - => new() { Culture = culture ?? string.Empty, Segment = segment ?? string.Empty, Value = value }; - } -} +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Cache; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Core.PropertyEditors; +// using Umbraco.Cms.Core.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Published; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class PublishedContentVarianceTests +// { +// private const string PropertyTypeAlias = "theProperty"; +// private const string DaCulture = "da-DK"; +// private const string EnCulture = "en-US"; +// private const string Segment1 = "segment1"; +// private const string Segment2 = "segment2"; +// +// [Test] +// public void No_Content_Variation_Can_Get_Invariant_Property() +// { +// var content = CreatePublishedContent(ContentVariation.Nothing, ContentVariation.Nothing); +// var value = GetPropertyValue(content); +// Assert.AreEqual("Invariant property value", value); +// } +// +// [TestCase(DaCulture)] +// [TestCase(EnCulture)] +// [TestCase("")] +// public void Content_Culture_Variation_Can_Get_Invariant_Property(string culture) +// { +// var content = CreatePublishedContent(ContentVariation.Culture, ContentVariation.Nothing, variationContextCulture: culture); +// var value = GetPropertyValue(content); +// Assert.AreEqual("Invariant property value", value); +// } +// +// [TestCase(Segment1)] +// [TestCase(Segment2)] +// [TestCase("")] +// public void Content_Segment_Variation_Can_Get_Invariant_Property(string segment) +// { +// var content = CreatePublishedContent(ContentVariation.Culture, ContentVariation.Nothing, variationContextSegment: segment); +// var value = GetPropertyValue(content); +// Assert.AreEqual("Invariant property value", value); +// } +// +// [TestCase(DaCulture, "DaDk property value")] +// [TestCase(EnCulture, "EnUs property value")] +// public void Content_Culture_Variation_Can_Get_Culture_Variant_Property(string culture, string expectedValue) +// { +// var content = CreatePublishedContent(ContentVariation.Culture, ContentVariation.Culture, variationContextCulture: culture); +// var value = GetPropertyValue(content); +// Assert.AreEqual(expectedValue, value); +// } +// +// [TestCase(Segment1, "Segment1 property value")] +// [TestCase(Segment2, "Segment2 property value")] +// public void Content_Segment_Variation_Can_Get_Segment_Variant_Property(string segment, string expectedValue) +// { +// var content = CreatePublishedContent(ContentVariation.Segment, ContentVariation.Segment, variationContextSegment: segment); +// var value = GetPropertyValue(content); +// Assert.AreEqual(expectedValue, value); +// } +// +// [TestCase(DaCulture, Segment1, "DaDk Segment1 property value")] +// [TestCase(DaCulture, Segment2, "DaDk Segment2 property value")] +// [TestCase(EnCulture, Segment1, "EnUs Segment1 property value")] +// [TestCase(EnCulture, Segment2, "EnUs Segment2 property value")] +// public void Content_Culture_And_Segment_Variation_Can_Get_Culture_And_Segment_Variant_Property(string culture, string segment, string expectedValue) +// { +// var content = CreatePublishedContent(ContentVariation.CultureAndSegment, ContentVariation.CultureAndSegment, variationContextCulture: culture, variationContextSegment: segment); +// var value = GetPropertyValue(content); +// Assert.AreEqual(expectedValue, value); +// } +// +// [TestCase(DaCulture, Segment1, "DaDk property value")] +// [TestCase(DaCulture, Segment2, "DaDk property value")] +// [TestCase(EnCulture, Segment1, "EnUs property value")] +// [TestCase(EnCulture, Segment2, "EnUs property value")] +// public void Content_Culture_And_Segment_Variation_Can_Get_Culture_Variant_Property(string culture, string segment, string expectedValue) +// { +// var content = CreatePublishedContent(ContentVariation.CultureAndSegment, ContentVariation.Culture, variationContextCulture: culture, variationContextSegment: segment); +// var value = GetPropertyValue(content); +// Assert.AreEqual(expectedValue, value); +// } +// +// [TestCase(DaCulture, Segment1, "Segment1 property value")] +// [TestCase(DaCulture, Segment2, "Segment2 property value")] +// [TestCase(EnCulture, Segment1, "Segment1 property value")] +// [TestCase(EnCulture, Segment2, "Segment2 property value")] +// public void Content_Culture_And_Segment_Variation_Can_Get_Segment_Variant_Property(string culture, string segment, string expectedValue) +// { +// var content = CreatePublishedContent(ContentVariation.CultureAndSegment, ContentVariation.Segment, variationContextCulture: culture, variationContextSegment: segment); +// var value = GetPropertyValue(content); +// Assert.AreEqual(expectedValue, value); +// } +// +// private object? GetPropertyValue(IPublishedContent content) => content.GetProperty(PropertyTypeAlias)!.GetValue(); +// +// private IPublishedContent CreatePublishedContent(ContentVariation contentTypeVariation, ContentVariation propertyTypeVariation, string? variationContextCulture = null, string? variationContextSegment = null) +// { +// var propertyType = new Mock(); +// propertyType.SetupGet(p => p.Alias).Returns(PropertyTypeAlias); +// propertyType.SetupGet(p => p.CacheLevel).Returns(PropertyCacheLevel.None); +// propertyType.SetupGet(p => p.DeliveryApiCacheLevel).Returns(PropertyCacheLevel.None); +// propertyType.SetupGet(p => p.Variations).Returns(propertyTypeVariation); +// propertyType +// .Setup(p => p.ConvertSourceToInter(It.IsAny(), It.IsAny(), It.IsAny())) +// .Returns((IPublishedElement _, object? source, bool _) => source); +// propertyType +// .Setup(p => p.ConvertInterToObject(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) +// .Returns((IPublishedElement _, PropertyCacheLevel _, object? inter, bool _) => inter); +// +// var contentType = new Mock(); +// contentType.SetupGet(c => c.PropertyTypes).Returns(new[] { propertyType.Object }); +// contentType.SetupGet(c => c.Variations).Returns(contentTypeVariation); +// +// var propertyData = new List(); +// +// switch (propertyTypeVariation) +// { +// case ContentVariation.Culture: +// propertyData.Add(CreatePropertyData("EnUs property value", culture: EnCulture)); +// propertyData.Add(CreatePropertyData("DaDk property value", culture: DaCulture)); +// break; +// case ContentVariation.Segment: +// propertyData.Add(CreatePropertyData("Segment1 property value", segment: Segment1)); +// propertyData.Add(CreatePropertyData("Segment2 property value", segment: Segment2)); +// break; +// case ContentVariation.CultureAndSegment: +// propertyData.Add(CreatePropertyData("EnUs Segment1 property value", culture: EnCulture, segment: Segment1)); +// propertyData.Add(CreatePropertyData("EnUs Segment2 property value", culture: EnCulture, segment: Segment2)); +// propertyData.Add(CreatePropertyData("DaDk Segment1 property value", culture: DaCulture, segment: Segment1)); +// propertyData.Add(CreatePropertyData("DaDk Segment2 property value", culture: DaCulture, segment: Segment2)); +// break; +// case ContentVariation.Nothing: +// propertyData.Add(CreatePropertyData("Invariant property value")); +// break; +// } +// +// var properties = new Dictionary { { PropertyTypeAlias, propertyData.ToArray() } }; +// +// var contentNode = new ContentNode(123, Guid.NewGuid(), contentType.Object, 1, string.Empty, 1, 1, DateTime.Now, 1); +// var contentData = new ContentData("bla", "bla", 1, DateTime.Now, 1, 1, true, properties, null); +// +// var elementCache = new FastDictionaryAppCache(); +// var snapshotCache = new FastDictionaryAppCache(); +// var publishedSnapshotMock = new Mock(); +// publishedSnapshotMock.SetupGet(p => p.ElementsCache).Returns(elementCache); +// publishedSnapshotMock.SetupGet(p => p.SnapshotCache).Returns(snapshotCache); +// +// var publishedSnapshot = publishedSnapshotMock.Object; +// var publishedSnapshotAccessor = new Mock(); +// publishedSnapshotAccessor.Setup(p => p.TryGetPublishedSnapshot(out publishedSnapshot)).Returns(true); +// +// var variationContextAccessorMock = new Mock(); +// variationContextAccessorMock +// .SetupGet(mock => mock.VariationContext) +// .Returns(() => new VariationContext(variationContextCulture, variationContextSegment)); +// +// return new PublishedContent( +// contentNode, +// contentData, +// publishedSnapshotAccessor.Object, +// variationContextAccessorMock.Object, +// Mock.Of()); +// +// PropertyData CreatePropertyData(string value, string? culture = null, string? segment = null) +// => new() { Culture = culture ?? string.Empty, Segment = segment ?? string.Empty, Value = value }; +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasTests.cs index 954d09b68a..0a02d2d2c7 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasTests.cs @@ -1,37 +1,38 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Routing; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -// TODO: We should be able to decouple this from the base db tests since we're just mocking the services now -[TestFixture] -public class ContentFinderByAliasTests : UrlRoutingTestBase -{ - [TestCase("/this/is/my/alias", 1001)] - [TestCase("/anotheralias", 1001)] - [TestCase("/page2/alias", 10011)] - [TestCase("/2ndpagealias", 10011)] - [TestCase("/only/one/alias", 100111)] - [TestCase("/ONLY/one/Alias", 100111)] - [TestCase("/alias43", 100121)] - public async Task Lookup_By_Url_Alias(string urlAsString, int nodeMatch) - { - var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - - var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - var lookup = - new ContentFinderByUrlAlias(Mock.Of>(), Mock.Of(), VariationContextAccessor, umbracoContextAccessor); - - var result = await lookup.TryFindContent(frequest); - - Assert.IsTrue(result); - Assert.AreEqual(frequest.PublishedContent.Id, nodeMatch); - } -} +// using System.Threading.Tasks; +// using Microsoft.Extensions.Logging; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// // TODO: We should be able to decouple this from the base db tests since we're just mocking the services now +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class ContentFinderByAliasTests : UrlRoutingTestBase +// { +// [TestCase("/this/is/my/alias", 1001)] +// [TestCase("/anotheralias", 1001)] +// [TestCase("/page2/alias", 10011)] +// [TestCase("/2ndpagealias", 10011)] +// [TestCase("/only/one/alias", 100111)] +// [TestCase("/ONLY/one/Alias", 100111)] +// [TestCase("/alias43", 100121)] +// public async Task Lookup_By_Url_Alias(string urlAsString, int nodeMatch) +// { +// var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString); +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// +// var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// var lookup = +// new ContentFinderByUrlAlias(Mock.Of>(), Mock.Of(), VariationContextAccessor, umbracoContextAccessor); +// +// var result = await lookup.TryFindContent(frequest); +// +// Assert.IsTrue(result); +// Assert.AreEqual(frequest.PublishedContent.Id, nodeMatch); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasWithDomainsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasWithDomainsTests.cs index 30bb4ae70c..48f298f709 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasWithDomainsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasWithDomainsTests.cs @@ -1,58 +1,59 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Routing; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class ContentFinderByAliasWithDomainsTests : UrlRoutingTestBase -{ - [TestCase("http://domain1.com/this/is/my/alias", "de-DE", -1001)] // alias to domain's page fails - no alias on domain's home - [TestCase("http://domain1.com/page2/alias", "de-DE", 10011)] // alias to sub-page works - [TestCase("http://domain1.com/en/flux", "en-US", -10011)] // alias to domain's page fails - no alias on domain's home - [TestCase("http://domain1.com/endanger", "de-DE", 10011)] // alias to sub-page works, even with "en..." - [TestCase("http://domain1.com/en/endanger", "en-US", -10011)] // no - [TestCase("http://domain1.com/only/one/alias", "de-DE", 100111)] // ok - [TestCase("http://domain1.com/entropy", "de-DE", 100111)] // ok - [TestCase("http://domain1.com/bar/foo", "de-DE", 100111)] // ok - [TestCase("http://domain1.com/en/bar/foo", "en-US", -100111)] // no, alias must include "en/" - [TestCase("http://domain1.com/en/bar/nil", "en-US", 100111)] // ok, alias includes "en/" - public async Task Lookup_By_Url_Alias_And_Domain(string inputUrl, string expectedCulture, int expectedNode) - { - // SetDomains1(); - var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - - var request = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - - // must lookup domain - publishedRouter.FindAndSetDomain(request); - - if (expectedNode > 0) - { - Assert.AreEqual(expectedCulture, request.Culture); - } - - var finder = new ContentFinderByUrlAlias( - Mock.Of>(), - Mock.Of(), - VariationContextAccessor, - umbracoContextAccessor); - var result = await finder.TryFindContent(request); - - if (expectedNode > 0) - { - Assert.IsTrue(result); - Assert.AreEqual(request.PublishedContent.Id, expectedNode); - } - else - { - Assert.IsFalse(result); - } - } -} +// using System.Threading.Tasks; +// using Microsoft.Extensions.Logging; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class ContentFinderByAliasWithDomainsTests : UrlRoutingTestBase +// { +// [TestCase("http://domain1.com/this/is/my/alias", "de-DE", -1001)] // alias to domain's page fails - no alias on domain's home +// [TestCase("http://domain1.com/page2/alias", "de-DE", 10011)] // alias to sub-page works +// [TestCase("http://domain1.com/en/flux", "en-US", -10011)] // alias to domain's page fails - no alias on domain's home +// [TestCase("http://domain1.com/endanger", "de-DE", 10011)] // alias to sub-page works, even with "en..." +// [TestCase("http://domain1.com/en/endanger", "en-US", -10011)] // no +// [TestCase("http://domain1.com/only/one/alias", "de-DE", 100111)] // ok +// [TestCase("http://domain1.com/entropy", "de-DE", 100111)] // ok +// [TestCase("http://domain1.com/bar/foo", "de-DE", 100111)] // ok +// [TestCase("http://domain1.com/en/bar/foo", "en-US", -100111)] // no, alias must include "en/" +// [TestCase("http://domain1.com/en/bar/nil", "en-US", 100111)] // ok, alias includes "en/" +// public async Task Lookup_By_Url_Alias_And_Domain(string inputUrl, string expectedCulture, int expectedNode) +// { +// // SetDomains1(); +// var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl); +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// +// var request = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// +// // must lookup domain +// publishedRouter.FindAndSetDomain(request); +// +// if (expectedNode > 0) +// { +// Assert.AreEqual(expectedCulture, request.Culture); +// } +// +// var finder = new ContentFinderByUrlAlias( +// Mock.Of>(), +// Mock.Of(), +// VariationContextAccessor, +// umbracoContextAccessor); +// var result = await finder.TryFindContent(request); +// +// if (expectedNode > 0) +// { +// Assert.IsTrue(result); +// Assert.AreEqual(request.PublishedContent.Id, expectedNode); +// } +// else +// { +// Assert.IsFalse(result); +// } +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdTests.cs index d752462853..52f457b5ef 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdTests.cs @@ -1,50 +1,51 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Web; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class ContentFinderByIdTests : ContentFinderByIdentifierTestsBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - } - - [TestCase("/1046", 1046, true)] - [TestCase("/1046", 1047, false)] - public async Task Lookup_By_Id(string urlAsString, int nodeId, bool shouldSucceed) - { - PopulateCache(nodeId, Guid.NewGuid()); - - var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); - var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - var webRoutingSettings = new WebRoutingSettings(); - var lookup = new ContentFinderByIdPath( - Mock.Of>(x => x.CurrentValue == webRoutingSettings), - Mock.Of>(), - Mock.Of(), - umbracoContextAccessor); - - var result = await lookup.TryFindContent(frequest); - - Assert.AreEqual(shouldSucceed, result); - if (shouldSucceed) - { - Assert.AreEqual(frequest.PublishedContent!.Id, nodeId); - } - else - { - Assert.IsNull(frequest.PublishedContent); - } - } -} +// using Microsoft.Extensions.Logging; +// using Microsoft.Extensions.Options; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Configuration.Models; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Cms.Core.Web; +// using Umbraco.Extensions; +// +// FIXME: Reintroduce if relevant +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// [TestFixture] +// public class ContentFinderByIdTests : ContentFinderByIdentifierTestsBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// } +// +// [TestCase("/1046", 1046, true)] +// [TestCase("/1046", 1047, false)] +// public async Task Lookup_By_Id(string urlAsString, int nodeId, bool shouldSucceed) +// { +// PopulateCache(nodeId, Guid.NewGuid()); +// +// var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); +// var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// var webRoutingSettings = new WebRoutingSettings(); +// var lookup = new ContentFinderByIdPath( +// Mock.Of>(x => x.CurrentValue == webRoutingSettings), +// Mock.Of>(), +// Mock.Of(), +// umbracoContextAccessor); +// +// var result = await lookup.TryFindContent(frequest); +// +// Assert.AreEqual(shouldSucceed, result); +// if (shouldSucceed) +// { +// Assert.AreEqual(frequest.PublishedContent!.Id, nodeId); +// } +// else +// { +// Assert.IsNull(frequest.PublishedContent); +// } +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdentifierTestsBase.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdentifierTestsBase.cs index 0da5aeb993..ebdc08a792 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdentifierTestsBase.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdentifierTestsBase.cs @@ -1,58 +1,59 @@ -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Builders.Extensions; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -public abstract class ContentFinderByIdentifierTestsBase : PublishedSnapshotServiceTestBase -{ - protected void PopulateCache(int nodeId, Guid nodeKey) - { - var dataTypes = GetDefaultDataTypes().Select(dt => dt as IDataType).ToArray(); - var propertyDataTypes = new Dictionary - { - // we only have one data type for this test which will be resolved with string empty. - [string.Empty] = dataTypes[0], - }; - IContentType contentType1 = new ContentType(ShortStringHelper, -1); - - var rootData = new ContentDataBuilder() - .WithName("Page" + Guid.NewGuid()) - .Build(ShortStringHelper, propertyDataTypes, contentType1, "alias"); - - var root = ContentNodeKitBuilder.CreateWithContent( - contentType1.Id, - 9876, - "-1,9876", - draftData: rootData, - publishedData: rootData); - - var parentData = new ContentDataBuilder() - .WithName("Page" + Guid.NewGuid()) - .Build(); - - var parent = ContentNodeKitBuilder.CreateWithContent( - contentType1.Id, - 5432, - "-1,9876,5432", - parentContentId: 9876, - draftData: parentData, - publishedData: parentData); - - var contentData = new ContentDataBuilder() - .WithName("Page" + Guid.NewGuid()) - .Build(); - - var content = ContentNodeKitBuilder.CreateWithContent( - contentType1.Id, - nodeId, - "-1,9876,5432," + nodeId, - parentContentId: 5432, - draftData: contentData, - publishedData: contentData, - uid: nodeKey); - - InitializedCache(new[] { root, parent, content }, [contentType1], dataTypes); - } -} +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Tests.Common.Builders; +// using Umbraco.Cms.Tests.Common.Builders.Extensions; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// public abstract class ContentFinderByIdentifierTestsBase : PublishedSnapshotServiceTestBase +// { +// protected void PopulateCache(int nodeId, Guid nodeKey) +// { +// var dataTypes = GetDefaultDataTypes().Select(dt => dt as IDataType).ToArray(); +// var propertyDataTypes = new Dictionary +// { +// // we only have one data type for this test which will be resolved with string empty. +// [string.Empty] = dataTypes[0], +// }; +// IContentType contentType1 = new ContentType(ShortStringHelper, -1); +// +// var rootData = new ContentDataBuilder() +// .WithName("Page" + Guid.NewGuid()) +// .Build(ShortStringHelper, propertyDataTypes, contentType1, "alias"); +// +// var root = ContentNodeKitBuilder.CreateWithContent( +// contentType1.Id, +// 9876, +// "-1,9876", +// draftData: rootData, +// publishedData: rootData); +// +// var parentData = new ContentDataBuilder() +// .WithName("Page" + Guid.NewGuid()) +// .Build(); +// +// var parent = ContentNodeKitBuilder.CreateWithContent( +// contentType1.Id, +// 5432, +// "-1,9876,5432", +// parentContentId: 9876, +// draftData: parentData, +// publishedData: parentData); +// +// var contentData = new ContentDataBuilder() +// .WithName("Page" + Guid.NewGuid()) +// .Build(); +// +// var content = ContentNodeKitBuilder.CreateWithContent( +// contentType1.Id, +// nodeId, +// "-1,9876,5432," + nodeId, +// parentContentId: 5432, +// draftData: contentData, +// publishedData: contentData, +// uid: nodeKey); +// +// InitializedCache(new[] { root, parent, content }, [contentType1], dataTypes); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByKeyTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByKeyTests.cs index 3d5b2990b2..a9b3771d86 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByKeyTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByKeyTests.cs @@ -1,52 +1,53 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Web; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class ContentFinderByKeyTests : ContentFinderByIdentifierTestsBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - } - - [TestCase("/1598901d-ebbe-4996-b7fb-6a6cbac13a62", "1598901d-ebbe-4996-b7fb-6a6cbac13a62", true)] - [TestCase("/1598901d-ebbe-4996-b7fb-6a6cbac13a62", "a383f6ed-cc54-46f1-a577-33f42e7214de", false)] - public async Task Lookup_By_Key(string urlAsString, string nodeKeyString, bool shouldSucceed) - { - var nodeKey = Guid.Parse(nodeKeyString); - - PopulateCache(9999, nodeKey); - - var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); - var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - var webRoutingSettings = new WebRoutingSettings(); - var lookup = new ContentFinderByKeyPath( - Mock.Of>(x => x.CurrentValue == webRoutingSettings), - Mock.Of>(), - Mock.Of(), - umbracoContextAccessor); - - var result = await lookup.TryFindContent(frequest); - - Assert.AreEqual(shouldSucceed, result); - if (shouldSucceed) - { - Assert.AreEqual(frequest.PublishedContent!.Key, nodeKey); - } - else - { - Assert.IsNull(frequest.PublishedContent); - } - } -} +// using Microsoft.Extensions.Logging; +// using Microsoft.Extensions.Options; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Configuration.Models; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Cms.Core.Web; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class ContentFinderByKeyTests : ContentFinderByIdentifierTestsBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// } +// +// [TestCase("/1598901d-ebbe-4996-b7fb-6a6cbac13a62", "1598901d-ebbe-4996-b7fb-6a6cbac13a62", true)] +// [TestCase("/1598901d-ebbe-4996-b7fb-6a6cbac13a62", "a383f6ed-cc54-46f1-a577-33f42e7214de", false)] +// public async Task Lookup_By_Key(string urlAsString, string nodeKeyString, bool shouldSucceed) +// { +// var nodeKey = Guid.Parse(nodeKeyString); +// +// PopulateCache(9999, nodeKey); +// +// var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); +// var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// var webRoutingSettings = new WebRoutingSettings(); +// var lookup = new ContentFinderByKeyPath( +// Mock.Of>(x => x.CurrentValue == webRoutingSettings), +// Mock.Of>(), +// Mock.Of(), +// umbracoContextAccessor); +// +// var result = await lookup.TryFindContent(frequest); +// +// Assert.AreEqual(shouldSucceed, result); +// if (shouldSucceed) +// { +// Assert.AreEqual(frequest.PublishedContent!.Key, nodeKey); +// } +// else +// { +// Assert.IsNull(frequest.PublishedContent); +// } +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByPageIdQueryTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByPageIdQueryTests.cs index 669b6b5dce..7c7993b0c0 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByPageIdQueryTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByPageIdQueryTests.cs @@ -1,60 +1,61 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Web; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class ContentFinderByPageIdQueryTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - var xml = PublishedContentXml.BaseWebTestXml(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - } - - [TestCase("/?umbPageId=1046", 1046)] - [TestCase("/?UMBPAGEID=1046", 1046)] - [TestCase("/default.aspx?umbPageId=1046", 1046)] // TODO: Should this match?? - [TestCase("/some/other/page?umbPageId=1046", 1046)] // TODO: Should this match?? - [TestCase("/some/other/page.aspx?umbPageId=1046", 1046)] // TODO: Should this match?? - public async Task Lookup_By_Page_Id(string urlAsString, int nodeMatch) - { - var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); - var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - - var queryStrings = HttpUtility.ParseQueryString(umbracoContext.CleanedUmbracoUrl.Query); - - var mockRequestAccessor = new Mock(); - mockRequestAccessor.Setup(x => x.GetRequestValue("umbPageID")) - .Returns(queryStrings["umbPageID"]); - - var lookup = new ContentFinderByPageIdQuery(mockRequestAccessor.Object, umbracoContextAccessor); - - var result = await lookup.TryFindContent(frequest); - - Assert.IsTrue(result); - Assert.AreEqual(frequest.PublishedContent.Id, nodeMatch); - } -} +// using System.Collections.Generic; +// using System.Linq; +// using System.Threading.Tasks; +// using System.Web; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Cms.Core.Web; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Published; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class ContentFinderByPageIdQueryTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// var xml = PublishedContentXml.BaseWebTestXml(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// } +// +// [TestCase("/?umbPageId=1046", 1046)] +// [TestCase("/?UMBPAGEID=1046", 1046)] +// [TestCase("/default.aspx?umbPageId=1046", 1046)] // TODO: Should this match?? +// [TestCase("/some/other/page?umbPageId=1046", 1046)] // TODO: Should this match?? +// [TestCase("/some/other/page.aspx?umbPageId=1046", 1046)] // TODO: Should this match?? +// public async Task Lookup_By_Page_Id(string urlAsString, int nodeMatch) +// { +// var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); +// var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// +// var queryStrings = HttpUtility.ParseQueryString(umbracoContext.CleanedUmbracoUrl.Query); +// +// var mockRequestAccessor = new Mock(); +// mockRequestAccessor.Setup(x => x.GetRequestValue("umbPageID")) +// .Returns(queryStrings["umbPageID"]); +// +// var lookup = new ContentFinderByPageIdQuery(mockRequestAccessor.Object, umbracoContextAccessor); +// +// var result = await lookup.TryFindContent(frequest); +// +// Assert.IsTrue(result); +// Assert.AreEqual(frequest.PublishedContent.Id, nodeMatch); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlAndTemplateTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlAndTemplateTests.cs index fee2a334c3..60639ee36b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlAndTemplateTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlAndTemplateTests.cs @@ -1,83 +1,84 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class ContentFinderByUrlAndTemplateTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - var xml = PublishedContentXml.BaseWebTestXml(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - } - - private IFileService _fileService; - - protected override ServiceContext CreateServiceContext(IContentType[] contentTypes, IMediaType[] mediaTypes, IDataType[] dataTypes) - { - var serviceContext = base.CreateServiceContext(contentTypes, mediaTypes, dataTypes); - - var fileService = Mock.Get(serviceContext.FileService); - fileService.Setup(x => x.GetTemplate(It.IsAny())) - .Returns((string alias) => new Template(ShortStringHelper, alias, alias)); - - _fileService = fileService.Object; - - return serviceContext; - } - - [TestCase("/blah")] - [TestCase("/home/Sub1/blah")] - [TestCase("/Home/Sub1/Blah")] // different cases - public async Task Match_Document_By_Url_With_Template(string urlAsString) - { - GlobalSettings.HideTopLevelNodeFromPath = false; - - var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); - var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - - var webRoutingSettings = new WebRoutingSettings(); - var lookup = new ContentFinderByUrlAndTemplate( - Mock.Of>(), - _fileService, - ContentTypeService, - umbracoContextAccessor, - Mock.Of>(x => x.CurrentValue == webRoutingSettings)); - - var result = await lookup.TryFindContent(frequest); - - var request = frequest.Build(); - - Assert.IsTrue(result); - Assert.IsNotNull(frequest.PublishedContent); - var templateAlias = request.GetTemplateAlias(); - Assert.IsNotNull(templateAlias); - Assert.AreEqual("blah".ToUpperInvariant(), templateAlias.ToUpperInvariant()); - } -} +// using System.Collections.Generic; +// using System.Linq; +// using System.Threading.Tasks; +// using Microsoft.Extensions.Logging; +// using Microsoft.Extensions.Options; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Configuration.Models; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Cms.Core.Services; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Published; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class ContentFinderByUrlAndTemplateTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// var xml = PublishedContentXml.BaseWebTestXml(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// } +// +// private IFileService _fileService; +// +// protected override ServiceContext CreateServiceContext(IContentType[] contentTypes, IMediaType[] mediaTypes, IDataType[] dataTypes) +// { +// var serviceContext = base.CreateServiceContext(contentTypes, mediaTypes, dataTypes); +// +// var fileService = Mock.Get(serviceContext.FileService); +// fileService.Setup(x => x.GetTemplate(It.IsAny())) +// .Returns((string alias) => new Template(ShortStringHelper, alias, alias)); +// +// _fileService = fileService.Object; +// +// return serviceContext; +// } +// +// [TestCase("/blah")] +// [TestCase("/home/Sub1/blah")] +// [TestCase("/Home/Sub1/Blah")] // different cases +// public async Task Match_Document_By_Url_With_Template(string urlAsString) +// { +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); +// var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// +// var webRoutingSettings = new WebRoutingSettings(); +// var lookup = new ContentFinderByUrlAndTemplate( +// Mock.Of>(), +// _fileService, +// ContentTypeService, +// umbracoContextAccessor, +// Mock.Of>(x => x.CurrentValue == webRoutingSettings)); +// +// var result = await lookup.TryFindContent(frequest); +// +// var request = frequest.Build(); +// +// Assert.IsTrue(result); +// Assert.IsNotNull(frequest.PublishedContent); +// var templateAlias = request.GetTemplateAlias(); +// Assert.IsNotNull(templateAlias); +// Assert.AreEqual("blah".ToUpperInvariant(), templateAlias.ToUpperInvariant()); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlTests.cs index 9ec7a305e7..ededa7144a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlTests.cs @@ -1,164 +1,165 @@ -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class ContentFinderByUrlTests : PublishedSnapshotServiceTestBase -{ - private async Task<(ContentFinderByUrl finder, IPublishedRequestBuilder frequest)> GetContentFinder( - string urlString) - { - var xml = PublishedContentXml.BaseWebTestXml(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - - var umbracoContextAccessor = GetUmbracoContextAccessor(urlString); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); - var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - var lookup = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); - return (lookup, frequest); - } - - [TestCase("/", 1046)] - [TestCase("/Sub1", 1173)] - [TestCase("/sub1", 1173)] - [TestCase("/home/sub1", -1)] // should fail - - // these two are special. getNiceUrl(1046) returns "/" but getNiceUrl(1172) cannot also return "/" so - // we've made it return "/test-page" => we have to support that URL back in the lookup... - [TestCase("/home", 1046)] - [TestCase("/test-page", 1172)] - public async Task Match_Document_By_Url_Hide_Top_Level(string urlString, int expectedId) - { - GlobalSettings.HideTopLevelNodeFromPath = true; - - var (finder, frequest) = await GetContentFinder(urlString); - - Assert.IsTrue(GlobalSettings.HideTopLevelNodeFromPath); - - // TODO: debugging - going further down, the routes cache is NOT empty?! - if (urlString == "/home/sub1") - { - Debugger.Break(); - } - - var result = await finder.TryFindContent(frequest); - - if (expectedId > 0) - { - Assert.IsTrue(result); - Assert.AreEqual(expectedId, frequest.PublishedContent.Id); - } - else - { - Assert.IsFalse(result); - } - } - - [TestCase("/", 1046)] - [TestCase("/home", 1046)] - [TestCase("/home/Sub1", 1173)] - [TestCase("/Home/Sub1", 1173)] // different cases - public async Task Match_Document_By_Url(string urlString, int expectedId) - { - GlobalSettings.HideTopLevelNodeFromPath = false; - - var (finder, frequest) = await GetContentFinder(urlString); - - Assert.IsFalse(GlobalSettings.HideTopLevelNodeFromPath); - - var result = await finder.TryFindContent(frequest); - - Assert.IsTrue(result); - Assert.AreEqual(expectedId, frequest.PublishedContent.Id); - } - - /// - /// This test handles requests with special characters in the URL. - /// - /// - /// - [TestCase("/", 1046)] - [TestCase("/home/sub1/custom-sub-3-with-accént-character", 1179)] - [TestCase("/home/sub1/custom-sub-4-with-æøå", 1180)] - public async Task Match_Document_By_Url_With_Special_Characters(string urlString, int expectedId) - { - GlobalSettings.HideTopLevelNodeFromPath = false; - - var (finder, frequest) = await GetContentFinder(urlString); - - var result = await finder.TryFindContent(frequest); - - Assert.IsTrue(result); - Assert.AreEqual(expectedId, frequest.PublishedContent.Id); - } - - /// - /// This test handles requests with a hostname associated. - /// The logic for handling this goes through the DomainHelper and is a bit different - /// from what happens in a normal request - so it has a separate test with a mocked - /// hostname added. - /// - /// - /// - [TestCase("/", 1046)] - [TestCase("/home/sub1/custom-sub-3-with-accént-character", 1179)] - [TestCase("/home/sub1/custom-sub-4-with-æøå", 1180)] - public async Task Match_Document_By_Url_With_Special_Characters_Using_Hostname(string urlString, int expectedId) - { - GlobalSettings.HideTopLevelNodeFromPath = false; - - var (finder, frequest) = await GetContentFinder(urlString); - - frequest.SetDomain(new DomainAndUri(new Domain(1, "mysite", -1, "en-US", false, 0), new Uri("http://mysite/"))); - - var result = await finder.TryFindContent(frequest); - - Assert.IsTrue(result); - Assert.AreEqual(expectedId, frequest.PublishedContent.Id); - } - - /// - /// This test handles requests with a hostname with special characters associated. - /// The logic for handling this goes through the DomainHelper and is a bit different - /// from what happens in a normal request - so it has a separate test with a mocked - /// hostname added. - /// - /// - /// - [TestCase("/æøå/", 1046)] - [TestCase("/æøå/home/sub1", 1173)] - [TestCase("/æøå/home/sub1/custom-sub-3-with-accént-character", 1179)] - [TestCase("/æøå/home/sub1/custom-sub-4-with-æøå", 1180)] - public async Task Match_Document_By_Url_With_Special_Characters_In_Hostname(string urlString, int expectedId) - { - GlobalSettings.HideTopLevelNodeFromPath = false; - - var (finder, frequest) = await GetContentFinder(urlString); - - frequest.SetDomain(new DomainAndUri(new Domain(1, "mysite/æøå", -1, "en-US", false, 0), new Uri("http://mysite/æøå"))); - - var result = await finder.TryFindContent(frequest); - - Assert.IsTrue(result); - Assert.AreEqual(expectedId, frequest.PublishedContent.Id); - } -} +// using System.Collections.Generic; +// using System.Diagnostics; +// using System.Linq; +// using System.Threading.Tasks; +// using Microsoft.Extensions.Logging; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Published; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class ContentFinderByUrlTests : PublishedSnapshotServiceTestBase +// { +// private async Task<(ContentFinderByUrl finder, IPublishedRequestBuilder frequest)> GetContentFinder( +// string urlString) +// { +// var xml = PublishedContentXml.BaseWebTestXml(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// var umbracoContextAccessor = GetUmbracoContextAccessor(urlString); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); +// var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// var lookup = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); +// return (lookup, frequest); +// } +// +// [TestCase("/", 1046)] +// [TestCase("/Sub1", 1173)] +// [TestCase("/sub1", 1173)] +// [TestCase("/home/sub1", -1)] // should fail +// +// // these two are special. getNiceUrl(1046) returns "/" but getNiceUrl(1172) cannot also return "/" so +// // we've made it return "/test-page" => we have to support that URL back in the lookup... +// [TestCase("/home", 1046)] +// [TestCase("/test-page", 1172)] +// public async Task Match_Document_By_Url_Hide_Top_Level(string urlString, int expectedId) +// { +// GlobalSettings.HideTopLevelNodeFromPath = true; +// +// var (finder, frequest) = await GetContentFinder(urlString); +// +// Assert.IsTrue(GlobalSettings.HideTopLevelNodeFromPath); +// +// // TODO: debugging - going further down, the routes cache is NOT empty?! +// if (urlString == "/home/sub1") +// { +// Debugger.Break(); +// } +// +// var result = await finder.TryFindContent(frequest); +// +// if (expectedId > 0) +// { +// Assert.IsTrue(result); +// Assert.AreEqual(expectedId, frequest.PublishedContent.Id); +// } +// else +// { +// Assert.IsFalse(result); +// } +// } +// +// [TestCase("/", 1046)] +// [TestCase("/home", 1046)] +// [TestCase("/home/Sub1", 1173)] +// [TestCase("/Home/Sub1", 1173)] // different cases +// public async Task Match_Document_By_Url(string urlString, int expectedId) +// { +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var (finder, frequest) = await GetContentFinder(urlString); +// +// Assert.IsFalse(GlobalSettings.HideTopLevelNodeFromPath); +// +// var result = await finder.TryFindContent(frequest); +// +// Assert.IsTrue(result); +// Assert.AreEqual(expectedId, frequest.PublishedContent.Id); +// } +// +// /// +// /// This test handles requests with special characters in the URL. +// /// +// /// +// /// +// [TestCase("/", 1046)] +// [TestCase("/home/sub1/custom-sub-3-with-accént-character", 1179)] +// [TestCase("/home/sub1/custom-sub-4-with-æøå", 1180)] +// public async Task Match_Document_By_Url_With_Special_Characters(string urlString, int expectedId) +// { +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var (finder, frequest) = await GetContentFinder(urlString); +// +// var result = await finder.TryFindContent(frequest); +// +// Assert.IsTrue(result); +// Assert.AreEqual(expectedId, frequest.PublishedContent.Id); +// } +// +// /// +// /// This test handles requests with a hostname associated. +// /// The logic for handling this goes through the DomainHelper and is a bit different +// /// from what happens in a normal request - so it has a separate test with a mocked +// /// hostname added. +// /// +// /// +// /// +// [TestCase("/", 1046)] +// [TestCase("/home/sub1/custom-sub-3-with-accént-character", 1179)] +// [TestCase("/home/sub1/custom-sub-4-with-æøå", 1180)] +// public async Task Match_Document_By_Url_With_Special_Characters_Using_Hostname(string urlString, int expectedId) +// { +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var (finder, frequest) = await GetContentFinder(urlString); +// +// frequest.SetDomain(new DomainAndUri(new Domain(1, "mysite", -1, "en-US", false, 0), new Uri("http://mysite/"))); +// +// var result = await finder.TryFindContent(frequest); +// +// Assert.IsTrue(result); +// Assert.AreEqual(expectedId, frequest.PublishedContent.Id); +// } +// +// /// +// /// This test handles requests with a hostname with special characters associated. +// /// The logic for handling this goes through the DomainHelper and is a bit different +// /// from what happens in a normal request - so it has a separate test with a mocked +// /// hostname added. +// /// +// /// +// /// +// [TestCase("/æøå/", 1046)] +// [TestCase("/æøå/home/sub1", 1173)] +// [TestCase("/æøå/home/sub1/custom-sub-3-with-accént-character", 1179)] +// [TestCase("/æøå/home/sub1/custom-sub-4-with-æøå", 1180)] +// public async Task Match_Document_By_Url_With_Special_Characters_In_Hostname(string urlString, int expectedId) +// { +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var (finder, frequest) = await GetContentFinder(urlString); +// +// frequest.SetDomain(new DomainAndUri(new Domain(1, "mysite/æøå", -1, "en-US", false, 0), new Uri("http://mysite/æøå"))); +// +// var result = await finder.TryFindContent(frequest); +// +// Assert.IsTrue(result); +// Assert.AreEqual(expectedId, frequest.PublishedContent.Id); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlWithDomainsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlWithDomainsTests.cs index 4606641265..25addd98e9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlWithDomainsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlWithDomainsTests.cs @@ -1,256 +1,257 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Routing; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class ContentFinderByUrlWithDomainsTests : UrlRoutingTestBase -{ - private void SetDomains3() - { - var domainService = Mock.Get(DomainService); - - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("domain1.com/") - { - Id = 1, LanguageId = LangDeId, RootContentId = 1001, - LanguageIsoCode = "de-DE", - }, - }); - } - - private void SetDomains4() - { - var domainService = Mock.Get(DomainService); - - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("domain1.com/") - { - Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US", - }, - new UmbracoDomain("domain1.com/en") - { - Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", - }, - new UmbracoDomain("domain1.com/fr") - { - Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", - }, - new UmbracoDomain("http://domain3.com/") - { - Id = 4, LanguageId = LangEngId, RootContentId = 1003, LanguageIsoCode = "en-US", - }, - new UmbracoDomain("http://domain3.com/en") - { - Id = 5, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US", - }, - new UmbracoDomain("http://domain3.com/fr") - { - Id = 6, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR", - }, - }); - } - - protected override string GetXmlContent(int templateId) - => @" - - -]> - - - - - This is some content]]> - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - - -"; - - [TestCase("http://domain1.com/", 1001)] - [TestCase("http://domain1.com/1001-1", 10011)] - [TestCase("http://domain1.com/1001-2/1001-2-1", 100121)] - public async Task Lookup_SingleDomain(string url, int expectedId) - { - SetDomains3(); - - GlobalSettings.HideTopLevelNodeFromPath = true; - - var umbracoContextAccessor = GetUmbracoContextAccessor(url); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - - // must lookup domain else lookup by URL fails - publishedRouter.FindAndSetDomain(frequest); - - var lookup = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); - var result = await lookup.TryFindContent(frequest); - Assert.IsTrue(result); - Assert.AreEqual(expectedId, frequest.PublishedContent.Id); - } - - [TestCase("http://domain1.com/", 1001, "en-US")] - [TestCase("http://domain1.com/en", 10011, "en-US")] - [TestCase("http://domain1.com/en/1001-1-1", 100111, "en-US")] - [TestCase("http://domain1.com/fr", 10012, "fr-FR")] - [TestCase("http://domain1.com/fr/1001-2-1", 100121, "fr-FR")] - [TestCase("http://domain1.com/1001-3", 10013, "en-US")] - [TestCase("http://domain2.com/1002", 1002, "")] - [TestCase("http://domain3.com/", 1003, "en-US")] - [TestCase("http://domain3.com/en", 10031, "en-US")] - [TestCase("http://domain3.com/en/1003-1-1", 100311, "en-US")] - [TestCase("http://domain3.com/fr", 10032, "fr-FR")] - [TestCase("http://domain3.com/fr/1003-2-1", 100321, "fr-FR")] - [TestCase("http://domain3.com/1003-3", 10033, "en-US")] - [TestCase("https://domain1.com/", 1001, "en-US")] - [TestCase("https://domain3.com/", 1001, "")] // because domain3 is explicitely set on http - public async Task Lookup_NestedDomains(string url, int expectedId, string expectedCulture) - { - SetDomains4(); - - // defaults depend on test environment - expectedCulture ??= Thread.CurrentThread.CurrentUICulture.Name; - - GlobalSettings.HideTopLevelNodeFromPath = true; - - var umbracoContextAccessor = GetUmbracoContextAccessor(url); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - - // must lookup domain else lookup by URL fails - publishedRouter.FindAndSetDomain(frequest); - Assert.AreEqual(expectedCulture, frequest.Culture); - - var lookup = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); - var result = await lookup.TryFindContent(frequest); - Assert.IsTrue(result); - Assert.AreEqual(expectedId, frequest.PublishedContent.Id); - } -} +// using System.Threading; +// using System.Threading.Tasks; +// using Microsoft.Extensions.Logging; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class ContentFinderByUrlWithDomainsTests : UrlRoutingTestBase +// { +// private void SetDomains3() +// { +// var domainService = Mock.Get(DomainService); +// +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("domain1.com/") +// { +// Id = 1, LanguageId = LangDeId, RootContentId = 1001, +// LanguageIsoCode = "de-DE", +// }, +// }); +// } +// +// private void SetDomains4() +// { +// var domainService = Mock.Get(DomainService); +// +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("domain1.com/") +// { +// Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US", +// }, +// new UmbracoDomain("domain1.com/en") +// { +// Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", +// }, +// new UmbracoDomain("domain1.com/fr") +// { +// Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", +// }, +// new UmbracoDomain("http://domain3.com/") +// { +// Id = 4, LanguageId = LangEngId, RootContentId = 1003, LanguageIsoCode = "en-US", +// }, +// new UmbracoDomain("http://domain3.com/en") +// { +// Id = 5, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US", +// }, +// new UmbracoDomain("http://domain3.com/fr") +// { +// Id = 6, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR", +// }, +// }); +// } +// +// protected override string GetXmlContent(int templateId) +// => @" +// +// +// ]> +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// "; +// +// [TestCase("http://domain1.com/", 1001)] +// [TestCase("http://domain1.com/1001-1", 10011)] +// [TestCase("http://domain1.com/1001-2/1001-2-1", 100121)] +// public async Task Lookup_SingleDomain(string url, int expectedId) +// { +// SetDomains3(); +// +// GlobalSettings.HideTopLevelNodeFromPath = true; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor(url); +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// +// // must lookup domain else lookup by URL fails +// publishedRouter.FindAndSetDomain(frequest); +// +// var lookup = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); +// var result = await lookup.TryFindContent(frequest); +// Assert.IsTrue(result); +// Assert.AreEqual(expectedId, frequest.PublishedContent.Id); +// } +// +// [TestCase("http://domain1.com/", 1001, "en-US")] +// [TestCase("http://domain1.com/en", 10011, "en-US")] +// [TestCase("http://domain1.com/en/1001-1-1", 100111, "en-US")] +// [TestCase("http://domain1.com/fr", 10012, "fr-FR")] +// [TestCase("http://domain1.com/fr/1001-2-1", 100121, "fr-FR")] +// [TestCase("http://domain1.com/1001-3", 10013, "en-US")] +// [TestCase("http://domain2.com/1002", 1002, "")] +// [TestCase("http://domain3.com/", 1003, "en-US")] +// [TestCase("http://domain3.com/en", 10031, "en-US")] +// [TestCase("http://domain3.com/en/1003-1-1", 100311, "en-US")] +// [TestCase("http://domain3.com/fr", 10032, "fr-FR")] +// [TestCase("http://domain3.com/fr/1003-2-1", 100321, "fr-FR")] +// [TestCase("http://domain3.com/1003-3", 10033, "en-US")] +// [TestCase("https://domain1.com/", 1001, "en-US")] +// [TestCase("https://domain3.com/", 1001, "")] // because domain3 is explicitely set on http +// public async Task Lookup_NestedDomains(string url, int expectedId, string expectedCulture) +// { +// SetDomains4(); +// +// // defaults depend on test environment +// expectedCulture ??= Thread.CurrentThread.CurrentUICulture.Name; +// +// GlobalSettings.HideTopLevelNodeFromPath = true; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor(url); +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// +// // must lookup domain else lookup by URL fails +// publishedRouter.FindAndSetDomain(frequest); +// Assert.AreEqual(expectedCulture, frequest.Culture); +// +// var lookup = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); +// var result = await lookup.TryFindContent(frequest); +// Assert.IsTrue(result); +// Assert.AreEqual(expectedId, frequest.PublishedContent.Id); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs index 9f0e2cbbf4..ef2195c09f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs @@ -1,359 +1,360 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Routing; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class DomainsAndCulturesTests : UrlRoutingTestBase -{ - private void SetDomains1() - { - var domainService = Mock.Get(DomainService); - - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("domain1.com/") - { - Id = 1, LanguageId = LangDeId, RootContentId = 1001, LanguageIsoCode = "de-DE", - }, - new UmbracoDomain("domain1.com/en") - { - Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", - }, - new UmbracoDomain("domain1.com/fr") - { - Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", - }, - }); - } - - private void SetDomains2() - { - var domainService = Mock.Get(DomainService); - - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("domain1.com/") - { - Id = 1, LanguageId = LangDeId, RootContentId = 1001, LanguageIsoCode = "de-DE", - }, - new UmbracoDomain("domain1.com/en") - { - Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", - }, - new UmbracoDomain("domain1.com/fr") - { - Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", - }, - new UmbracoDomain("*1001") - { - Id = 4, LanguageId = LangDeId, RootContentId = 1001, LanguageIsoCode = "de-DE", - }, - new UmbracoDomain("*10011") - { - Id = 5, LanguageId = LangCzId, RootContentId = 10011, LanguageIsoCode = "cs-CZ", - }, - new UmbracoDomain("*100112") - { - Id = 6, LanguageId = LangNlId, RootContentId = 100112, LanguageIsoCode = "nl-NL", - }, - new UmbracoDomain("*1001122") - { - Id = 7, LanguageId = LangDkId, RootContentId = 1001122, LanguageIsoCode = "da-DK", - }, - new UmbracoDomain("*10012") - { - Id = 8, LanguageId = LangNlId, RootContentId = 10012, LanguageIsoCode = "nl-NL", - }, - new UmbracoDomain("*10031") - { - Id = 9, LanguageId = LangNlId, RootContentId = 10031, LanguageIsoCode = "nl-NL", - }, - }); - } - - // domains such as "/en" are natively supported, and when instanciating - // DomainAndUri for them, the host will come from the current request - private void SetDomains3() - { - var domainService = Mock.Get(DomainService); - - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("/en") - { - Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", - }, - new UmbracoDomain("/fr") - { - Id = 2, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", - }, - }); - } - - protected override string GetXmlContent(int templateId) - => @" - - -]> - - - - - - This is some content]]> - - - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - - -"; - - #region Cases - - [TestCase("http://domain1.com/", "de-DE", 1001)] - [TestCase("http://domain1.com/1001-1", "de-DE", 10011)] - [TestCase("http://domain1.com/1001-1/1001-1-1", "de-DE", 100111)] - [TestCase("http://domain1.com/en", "en-US", 10011)] - [TestCase("http://domain1.com/en/1001-1-1", "en-US", 100111)] - [TestCase("http://domain1.com/fr", "fr-FR", 10012)] - [TestCase("http://domain1.com/fr/1001-2-1", "fr-FR", 100121)] - - #endregion - - public async Task DomainAndCulture(string inputUrl, string expectedCulture, int expectedNode) - { - SetDomains1(); - - GlobalSettings.HideTopLevelNodeFromPath = false; - - var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - - // lookup domain - publishedRouter.FindAndSetDomain(frequest); - - Assert.AreEqual(expectedCulture, frequest.Culture); - - var finder = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); - var result = await finder.TryFindContent(frequest); - - Assert.IsTrue(result); - Assert.AreEqual(frequest.PublishedContent.Id, expectedNode); - } - - #region Cases - - [TestCase("http://domain1.com/", "de-DE", 1001)] // domain takes over local wildcard at 1001 - [TestCase("http://domain1.com/1001-1", "cs-CZ", 10011)] // wildcard on 10011 applies - [TestCase("http://domain1.com/1001-1/1001-1-1", "cs-CZ", 100111)] // wildcard on 10011 applies - [TestCase("http://domain1.com/1001-1/1001-1-2", "nl-NL", 100112)] // wildcard on 100112 applies - [TestCase("http://domain1.com/1001-1/1001-1-2/1001-1-2-1", "nl-NL", 1001121)] // wildcard on 100112 applies - [TestCase("http://domain1.com/1001-1/1001-1-2/1001-1-2-2", "da-DK", 1001122)] // wildcard on 1001122 applies - [TestCase("http://domain1.com/1001-2", "nl-NL", 10012)] // wildcard on 10012 applies - [TestCase("http://domain1.com/1001-2/1001-2-1", "nl-NL", 100121)] // wildcard on 10012 applies - [TestCase("http://domain1.com/en", "en-US", 10011)] // domain takes over local wildcard at 10011 - [TestCase("http://domain1.com/en/1001-1-1", "en-US", 100111)] // domain takes over local wildcard at 10011 - [TestCase("http://domain1.com/en/1001-1-2", "nl-NL", 100112)] // wildcard on 100112 applies - [TestCase("http://domain1.com/en/1001-1-2/1001-1-2-1", "nl-NL", 1001121)] // wildcard on 100112 applies - [TestCase("http://domain1.com/en/1001-1-2/1001-1-2-2", "da-DK", 1001122)] // wildcard on 1001122 applies - [TestCase("http://domain1.com/fr", "fr-FR", 10012)] // domain takes over local wildcard at 10012 - [TestCase("http://domain1.com/fr/1001-2-1", "fr-FR", 100121)] // domain takes over local wildcard at 10012 - [TestCase("/1003", "", 1003)] // default culture (no domain) - [TestCase("/1003/1003-1", "nl-NL", 10031)] // wildcard on 10031 applies - [TestCase("/1003/1003-1/1003-1-1", "nl-NL", 100311)] // wildcard on 10031 applies - - #endregion - - public async Task DomainAndCultureWithWildcards(string inputUrl, string expectedCulture, int expectedNode) - { - SetDomains2(); - - // defaults depend on test environment - expectedCulture ??= Thread.CurrentThread.CurrentUICulture.Name; - - GlobalSettings.HideTopLevelNodeFromPath = false; - - var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor, domainCache: umbracoContextAccessor.GetRequiredUmbracoContext().PublishedSnapshot.Domains); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - - // lookup domain - publishedRouter.FindAndSetDomain(frequest); - - // find document - var finder = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); - var result = await finder.TryFindContent(frequest); - - // apply wildcard domain - publishedRouter.HandleWildcardDomains(frequest); - - Assert.IsTrue(result); - Assert.AreEqual(expectedCulture, frequest.Culture); - Assert.AreEqual(frequest.PublishedContent.Id, expectedNode); - } - - #region Cases - - [TestCase("http://domain1.com/en", "en-US", 10011)] - [TestCase("http://domain1.com/en/1001-1-1", "en-US", 100111)] - [TestCase("http://domain1.com/fr", "fr-FR", 10012)] - [TestCase("http://domain1.com/fr/1001-2-1", "fr-FR", 100121)] - - #endregion - - public async Task DomainGeneric(string inputUrl, string expectedCulture, int expectedNode) - { - SetDomains3(); - - GlobalSettings.HideTopLevelNodeFromPath = false; - - var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl); - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - - // lookup domain - publishedRouter.FindAndSetDomain(frequest); - Assert.IsNotNull(frequest.Domain); - - Assert.AreEqual(expectedCulture, frequest.Culture); - - var finder = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); - var result = await finder.TryFindContent(frequest); - - Assert.IsTrue(result); - Assert.AreEqual(frequest.PublishedContent.Id, expectedNode); - } -} +// using System.Threading; +// using System.Threading.Tasks; +// using Microsoft.Extensions.Logging; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class DomainsAndCulturesTests : UrlRoutingTestBase +// { +// private void SetDomains1() +// { +// var domainService = Mock.Get(DomainService); +// +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("domain1.com/") +// { +// Id = 1, LanguageId = LangDeId, RootContentId = 1001, LanguageIsoCode = "de-DE", +// }, +// new UmbracoDomain("domain1.com/en") +// { +// Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", +// }, +// new UmbracoDomain("domain1.com/fr") +// { +// Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", +// }, +// }); +// } +// +// private void SetDomains2() +// { +// var domainService = Mock.Get(DomainService); +// +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("domain1.com/") +// { +// Id = 1, LanguageId = LangDeId, RootContentId = 1001, LanguageIsoCode = "de-DE", +// }, +// new UmbracoDomain("domain1.com/en") +// { +// Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", +// }, +// new UmbracoDomain("domain1.com/fr") +// { +// Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", +// }, +// new UmbracoDomain("*1001") +// { +// Id = 4, LanguageId = LangDeId, RootContentId = 1001, LanguageIsoCode = "de-DE", +// }, +// new UmbracoDomain("*10011") +// { +// Id = 5, LanguageId = LangCzId, RootContentId = 10011, LanguageIsoCode = "cs-CZ", +// }, +// new UmbracoDomain("*100112") +// { +// Id = 6, LanguageId = LangNlId, RootContentId = 100112, LanguageIsoCode = "nl-NL", +// }, +// new UmbracoDomain("*1001122") +// { +// Id = 7, LanguageId = LangDkId, RootContentId = 1001122, LanguageIsoCode = "da-DK", +// }, +// new UmbracoDomain("*10012") +// { +// Id = 8, LanguageId = LangNlId, RootContentId = 10012, LanguageIsoCode = "nl-NL", +// }, +// new UmbracoDomain("*10031") +// { +// Id = 9, LanguageId = LangNlId, RootContentId = 10031, LanguageIsoCode = "nl-NL", +// }, +// }); +// } +// +// // domains such as "/en" are natively supported, and when instanciating +// // DomainAndUri for them, the host will come from the current request +// private void SetDomains3() +// { +// var domainService = Mock.Get(DomainService); +// +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("/en") +// { +// Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", +// }, +// new UmbracoDomain("/fr") +// { +// Id = 2, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", +// }, +// }); +// } +// +// protected override string GetXmlContent(int templateId) +// => @" +// +// +// ]> +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// "; +// +// #region Cases +// +// [TestCase("http://domain1.com/", "de-DE", 1001)] +// [TestCase("http://domain1.com/1001-1", "de-DE", 10011)] +// [TestCase("http://domain1.com/1001-1/1001-1-1", "de-DE", 100111)] +// [TestCase("http://domain1.com/en", "en-US", 10011)] +// [TestCase("http://domain1.com/en/1001-1-1", "en-US", 100111)] +// [TestCase("http://domain1.com/fr", "fr-FR", 10012)] +// [TestCase("http://domain1.com/fr/1001-2-1", "fr-FR", 100121)] +// +// #endregion +// +// public async Task DomainAndCulture(string inputUrl, string expectedCulture, int expectedNode) +// { +// SetDomains1(); +// +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl); +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// +// // lookup domain +// publishedRouter.FindAndSetDomain(frequest); +// +// Assert.AreEqual(expectedCulture, frequest.Culture); +// +// var finder = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); +// var result = await finder.TryFindContent(frequest); +// +// Assert.IsTrue(result); +// Assert.AreEqual(frequest.PublishedContent.Id, expectedNode); +// } +// +// #region Cases +// +// [TestCase("http://domain1.com/", "de-DE", 1001)] // domain takes over local wildcard at 1001 +// [TestCase("http://domain1.com/1001-1", "cs-CZ", 10011)] // wildcard on 10011 applies +// [TestCase("http://domain1.com/1001-1/1001-1-1", "cs-CZ", 100111)] // wildcard on 10011 applies +// [TestCase("http://domain1.com/1001-1/1001-1-2", "nl-NL", 100112)] // wildcard on 100112 applies +// [TestCase("http://domain1.com/1001-1/1001-1-2/1001-1-2-1", "nl-NL", 1001121)] // wildcard on 100112 applies +// [TestCase("http://domain1.com/1001-1/1001-1-2/1001-1-2-2", "da-DK", 1001122)] // wildcard on 1001122 applies +// [TestCase("http://domain1.com/1001-2", "nl-NL", 10012)] // wildcard on 10012 applies +// [TestCase("http://domain1.com/1001-2/1001-2-1", "nl-NL", 100121)] // wildcard on 10012 applies +// [TestCase("http://domain1.com/en", "en-US", 10011)] // domain takes over local wildcard at 10011 +// [TestCase("http://domain1.com/en/1001-1-1", "en-US", 100111)] // domain takes over local wildcard at 10011 +// [TestCase("http://domain1.com/en/1001-1-2", "nl-NL", 100112)] // wildcard on 100112 applies +// [TestCase("http://domain1.com/en/1001-1-2/1001-1-2-1", "nl-NL", 1001121)] // wildcard on 100112 applies +// [TestCase("http://domain1.com/en/1001-1-2/1001-1-2-2", "da-DK", 1001122)] // wildcard on 1001122 applies +// [TestCase("http://domain1.com/fr", "fr-FR", 10012)] // domain takes over local wildcard at 10012 +// [TestCase("http://domain1.com/fr/1001-2-1", "fr-FR", 100121)] // domain takes over local wildcard at 10012 +// [TestCase("/1003", "", 1003)] // default culture (no domain) +// [TestCase("/1003/1003-1", "nl-NL", 10031)] // wildcard on 10031 applies +// [TestCase("/1003/1003-1/1003-1-1", "nl-NL", 100311)] // wildcard on 10031 applies +// +// #endregion +// +// public async Task DomainAndCultureWithWildcards(string inputUrl, string expectedCulture, int expectedNode) +// { +// SetDomains2(); +// +// // defaults depend on test environment +// expectedCulture ??= Thread.CurrentThread.CurrentUICulture.Name; +// +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl); +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor, domainCache: umbracoContextAccessor.GetRequiredUmbracoContext().PublishedSnapshot.Domains); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// +// // lookup domain +// publishedRouter.FindAndSetDomain(frequest); +// +// // find document +// var finder = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); +// var result = await finder.TryFindContent(frequest); +// +// // apply wildcard domain +// publishedRouter.HandleWildcardDomains(frequest); +// +// Assert.IsTrue(result); +// Assert.AreEqual(expectedCulture, frequest.Culture); +// Assert.AreEqual(frequest.PublishedContent.Id, expectedNode); +// } +// +// #region Cases +// +// [TestCase("http://domain1.com/en", "en-US", 10011)] +// [TestCase("http://domain1.com/en/1001-1-1", "en-US", 100111)] +// [TestCase("http://domain1.com/fr", "fr-FR", 10012)] +// [TestCase("http://domain1.com/fr/1001-2-1", "fr-FR", 100121)] +// +// #endregion +// +// public async Task DomainGeneric(string inputUrl, string expectedCulture, int expectedNode) +// { +// SetDomains3(); +// +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl); +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// +// // lookup domain +// publishedRouter.FindAndSetDomain(frequest); +// Assert.IsNotNull(frequest.Domain); +// +// Assert.AreEqual(expectedCulture, frequest.Culture); +// +// var finder = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); +// var result = await finder.TryFindContent(frequest); +// +// Assert.IsTrue(result); +// Assert.AreEqual(frequest.PublishedContent.Id, expectedNode); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/GetContentUrlsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/GetContentUrlsTests.cs index 8a7f2ff66f..659e8116d3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/GetContentUrlsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/GetContentUrlsTests.cs @@ -1,199 +1,200 @@ -using System.Globalization; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class GetContentUrlsTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - _webRoutingSettings = new WebRoutingSettings(); - _requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - - GlobalSettings.HideTopLevelNodeFromPath = false; - - var xml = PublishedContentXml.BaseWebTestXml(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - } - - private WebRoutingSettings _webRoutingSettings; - private RequestHandlerSettings _requestHandlerSettings; - - private ILocalizedTextService GetTextService() - { - var textService = new Mock(); - textService.Setup(x => x.Localize( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>())) - .Returns((string key, string alias, CultureInfo culture, IDictionary args) - => $"{key}/{alias}"); - - return textService.Object; - } - - private ILanguageService GetLangService(params string[] isoCodes) - { - var allLangs = isoCodes - .Select(CultureInfo.GetCultureInfo) - .Select(culture => new Language(culture.Name, culture.EnglishName) { IsDefault = true, IsMandatory = true }) - .ToArray(); - - var langServiceMock = new Mock(); - langServiceMock.Setup(x => x.GetAllAsync()).ReturnsAsync(allLangs); - langServiceMock.Setup(x => x.GetDefaultIsoCodeAsync()).ReturnsAsync(allLangs.First(x => x.IsDefault).IsoCode); - - return langServiceMock.Object; - } - - [Test] - public async Task Content_Not_Published() - { - var contentType = ContentTypeBuilder.CreateBasicContentType(); - var content = ContentBuilder.CreateBasicContent(contentType); - content.Id = 1046; // TODO: we are using this ID only because it's built into the test XML published cache - content.Path = "-1,1046"; - - var umbracoContextAccessor = GetUmbracoContextAccessor("http://localhost:8000"); - var publishedRouter = CreatePublishedRouter( - umbracoContextAccessor, - new[] { new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor) }); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - - var urlProvider = GetUrlProvider(umbracoContextAccessor, _requestHandlerSettings, _webRoutingSettings, out var uriUtility); - - var urls = (await content.GetContentUrlsAsync( - publishedRouter, - umbracoContext, - GetLangService("en-US", "fr-FR"), - GetTextService(), - Mock.Of(), - VariationContextAccessor, - Mock.Of>(), - uriUtility, - urlProvider)).ToList(); - - Assert.AreEqual(1, urls.Count); - Assert.AreEqual("content/itemNotPublished", urls[0].Text); - Assert.IsFalse(urls[0].IsUrl); - } - - [Test] - public async Task Invariant_Root_Content_Published_No_Domains() - { - var contentType = ContentTypeBuilder.CreateBasicContentType(); - var content = ContentBuilder.CreateBasicContent(contentType); - content.Id = 1046; // TODO: we are using this ID only because it's built into the test XML published cache - content.Path = "-1,1046"; - content.Published = true; - - var umbracoContextAccessor = GetUmbracoContextAccessor("http://localhost:8000"); - var publishedRouter = CreatePublishedRouter( - umbracoContextAccessor, - new[] { new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor) }); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - - var urlProvider = GetUrlProvider(umbracoContextAccessor, _requestHandlerSettings, _webRoutingSettings, out var uriUtility); - - var urls = (await content.GetContentUrlsAsync( - publishedRouter, - umbracoContext, - GetLangService("en-US", "fr-FR"), - GetTextService(), - Mock.Of(), - VariationContextAccessor, - Mock.Of>(), - uriUtility, - urlProvider)).ToList(); - - Assert.AreEqual(2, urls.Count); - - var enUrl = urls.First(x => x.Culture == "en-US"); - - Assert.AreEqual("/home/", enUrl.Text); - Assert.AreEqual("en-US", enUrl.Culture); - Assert.IsTrue(enUrl.IsUrl); - - var frUrl = urls.First(x => x.Culture == "fr-FR"); - - Assert.IsFalse(frUrl.IsUrl); - } - - [Test] - public async Task Invariant_Child_Content_Published_No_Domains() - { - var contentType = ContentTypeBuilder.CreateBasicContentType(); - var parent = ContentBuilder.CreateBasicContent(contentType); - parent.Id = 1046; // TODO: we are using this ID only because it's built into the test XML published cache - parent.Name = "home"; - parent.Path = "-1,1046"; - parent.Published = true; - var child = ContentBuilder.CreateBasicContent(contentType); - child.Name = "sub1"; - child.Id = 1173; // TODO: we are using this ID only because it's built into the test XML published cache - child.Path = "-1,1046,1173"; - child.Published = true; - - var umbracoContextAccessor = GetUmbracoContextAccessor("http://localhost:8000"); - var publishedRouter = CreatePublishedRouter( - umbracoContextAccessor, - new[] { new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor) }); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - - var localizationService = GetLangService("en-US", "fr-FR"); - var urlProvider = GetUrlProvider(umbracoContextAccessor, _requestHandlerSettings, _webRoutingSettings, out var uriUtility); - - var urls = (await child.GetContentUrlsAsync( - publishedRouter, - umbracoContext, - localizationService, - GetTextService(), - Mock.Of(), - VariationContextAccessor, - Mock.Of>(), - uriUtility, - urlProvider)).ToList(); - - Assert.AreEqual(2, urls.Count); - - var enUrl = urls.First(x => x.Culture == "en-US"); - - Assert.AreEqual("/home/sub1/", enUrl.Text); - Assert.AreEqual("en-US", enUrl.Culture); - Assert.IsTrue(enUrl.IsUrl); - - var frUrl = urls.First(x => x.Culture == "fr-FR"); - - Assert.IsFalse(frUrl.IsUrl); - } - - // TODO: We need a lot of tests here, the above was just to get started with being able to unit test this method - // * variant URLs without domains assigned, what happens? - // * variant URLs with domains assigned, but also having more languages installed than there are domains/cultures assigned - // * variant URLs with an ancestor culture unpublished - // * invariant URLs with ancestors as variants - // * ... probably a lot more -} +// using System.Globalization; +// using Microsoft.Extensions.Logging; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Configuration.Models; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Cms.Core.Services; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Builders; +// using Umbraco.Cms.Tests.Common.Published; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class GetContentUrlsTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// _webRoutingSettings = new WebRoutingSettings(); +// _requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var xml = PublishedContentXml.BaseWebTestXml(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// } +// +// private WebRoutingSettings _webRoutingSettings; +// private RequestHandlerSettings _requestHandlerSettings; +// +// private ILocalizedTextService GetTextService() +// { +// var textService = new Mock(); +// textService.Setup(x => x.Localize( +// It.IsAny(), +// It.IsAny(), +// It.IsAny(), +// It.IsAny>())) +// .Returns((string key, string alias, CultureInfo culture, IDictionary args) +// => $"{key}/{alias}"); +// +// return textService.Object; +// } +// +// private ILanguageService GetLangService(params string[] isoCodes) +// { +// var allLangs = isoCodes +// .Select(CultureInfo.GetCultureInfo) +// .Select(culture => new Language(culture.Name, culture.EnglishName) { IsDefault = true, IsMandatory = true }) +// .ToArray(); +// +// var langServiceMock = new Mock(); +// langServiceMock.Setup(x => x.GetAllAsync()).ReturnsAsync(allLangs); +// langServiceMock.Setup(x => x.GetDefaultIsoCodeAsync()).ReturnsAsync(allLangs.First(x => x.IsDefault).IsoCode); +// +// return langServiceMock.Object; +// } +// +// [Test] +// public async Task Content_Not_Published() +// { +// var contentType = ContentTypeBuilder.CreateBasicContentType(); +// var content = ContentBuilder.CreateBasicContent(contentType); +// content.Id = 1046; // TODO: we are using this ID only because it's built into the test XML published cache +// content.Path = "-1,1046"; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("http://localhost:8000"); +// var publishedRouter = CreatePublishedRouter( +// umbracoContextAccessor, +// new[] { new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor) }); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// +// var urlProvider = GetUrlProvider(umbracoContextAccessor, _requestHandlerSettings, _webRoutingSettings, out var uriUtility); +// +// var urls = (await content.GetContentUrlsAsync( +// publishedRouter, +// umbracoContext, +// GetLangService("en-US", "fr-FR"), +// GetTextService(), +// Mock.Of(), +// VariationContextAccessor, +// Mock.Of>(), +// uriUtility, +// urlProvider)).ToList(); +// +// Assert.AreEqual(1, urls.Count); +// Assert.AreEqual("content/itemNotPublished", urls[0].Text); +// Assert.IsFalse(urls[0].IsUrl); +// } +// +// [Test] +// public async Task Invariant_Root_Content_Published_No_Domains() +// { +// var contentType = ContentTypeBuilder.CreateBasicContentType(); +// var content = ContentBuilder.CreateBasicContent(contentType); +// content.Id = 1046; // TODO: we are using this ID only because it's built into the test XML published cache +// content.Path = "-1,1046"; +// content.Published = true; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("http://localhost:8000"); +// var publishedRouter = CreatePublishedRouter( +// umbracoContextAccessor, +// new[] { new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor) }); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// +// var urlProvider = GetUrlProvider(umbracoContextAccessor, _requestHandlerSettings, _webRoutingSettings, out var uriUtility); +// +// var urls = (await content.GetContentUrlsAsync( +// publishedRouter, +// umbracoContext, +// GetLangService("en-US", "fr-FR"), +// GetTextService(), +// Mock.Of(), +// VariationContextAccessor, +// Mock.Of>(), +// uriUtility, +// urlProvider)).ToList(); +// +// Assert.AreEqual(2, urls.Count); +// +// var enUrl = urls.First(x => x.Culture == "en-US"); +// +// Assert.AreEqual("/home/", enUrl.Text); +// Assert.AreEqual("en-US", enUrl.Culture); +// Assert.IsTrue(enUrl.IsUrl); +// +// var frUrl = urls.First(x => x.Culture == "fr-FR"); +// +// Assert.IsFalse(frUrl.IsUrl); +// } +// +// [Test] +// public async Task Invariant_Child_Content_Published_No_Domains() +// { +// var contentType = ContentTypeBuilder.CreateBasicContentType(); +// var parent = ContentBuilder.CreateBasicContent(contentType); +// parent.Id = 1046; // TODO: we are using this ID only because it's built into the test XML published cache +// parent.Name = "home"; +// parent.Path = "-1,1046"; +// parent.Published = true; +// var child = ContentBuilder.CreateBasicContent(contentType); +// child.Name = "sub1"; +// child.Id = 1173; // TODO: we are using this ID only because it's built into the test XML published cache +// child.Path = "-1,1046,1173"; +// child.Published = true; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("http://localhost:8000"); +// var publishedRouter = CreatePublishedRouter( +// umbracoContextAccessor, +// new[] { new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor) }); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// +// var localizationService = GetLangService("en-US", "fr-FR"); +// var urlProvider = GetUrlProvider(umbracoContextAccessor, _requestHandlerSettings, _webRoutingSettings, out var uriUtility); +// +// var urls = (await child.GetContentUrlsAsync( +// publishedRouter, +// umbracoContext, +// localizationService, +// GetTextService(), +// Mock.Of(), +// VariationContextAccessor, +// Mock.Of>(), +// uriUtility, +// urlProvider)).ToList(); +// +// Assert.AreEqual(2, urls.Count); +// +// var enUrl = urls.First(x => x.Culture == "en-US"); +// +// Assert.AreEqual("/home/sub1/", enUrl.Text); +// Assert.AreEqual("en-US", enUrl.Culture); +// Assert.IsTrue(enUrl.IsUrl); +// +// var frUrl = urls.First(x => x.Culture == "fr-FR"); +// +// Assert.IsFalse(frUrl.IsUrl); +// } +// +// // TODO: We need a lot of tests here, the above was just to get started with being able to unit test this method +// // * variant URLs without domains assigned, what happens? +// // * variant URLs with domains assigned, but also having more languages installed than there are domains/cultures assigned +// // * variant URLs with an ancestor culture unpublished +// // * invariant URLs with ancestors as variants +// // * ... probably a lot more +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/PublishedRouterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/PublishedRouterTests.cs index b56e55ff67..44f660a9c6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/PublishedRouterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/PublishedRouterTests.cs @@ -86,7 +86,6 @@ public class PublishedRouterTests pc.Setup(content => content.CreateDate).Returns(DateTime.Now); pc.Setup(content => content.UpdateDate).Returns(DateTime.Now); pc.Setup(content => content.Path).Returns("-1,1"); - pc.Setup(content => content.Parent).Returns(() => null); pc.Setup(content => content.Properties).Returns(new Collection()); pc.Setup(content => content.ContentType) .Returns(new PublishedContentType( diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlProviderWithHideTopLevelNodeFromPathTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlProviderWithHideTopLevelNodeFromPathTests.cs index e8df94c196..71a50d6533 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlProviderWithHideTopLevelNodeFromPathTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlProviderWithHideTopLevelNodeFromPathTests.cs @@ -1,51 +1,52 @@ -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class UrlProviderWithHideTopLevelNodeFromPathTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - var xml = PublishedContentXml.BaseWebTestXml(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - - GlobalSettings.HideTopLevelNodeFromPath = true; - } - - [TestCase(1046, "/")] - [TestCase(1173, "/sub1/")] - [TestCase(1174, "/sub1/sub2/")] - [TestCase(1176, "/sub1/sub-3/")] - [TestCase(1177, "/sub1/custom-sub-1/")] - [TestCase(1178, "/sub1/custom-sub-2/")] - [TestCase(1175, "/sub-2/")] - [TestCase(1172, "/test-page/")] // not hidden because not first root - public void Get_Url_Hiding_Top_Level(int nodeId, string niceUrlMatch) - { - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - - var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); - - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out var uriUtility); - - var result = urlProvider.GetUrl(nodeId); - Assert.AreEqual(niceUrlMatch, result); - } -} +// using System.Collections.Generic; +// using System.Linq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Configuration.Models; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Published; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class UrlProviderWithHideTopLevelNodeFromPathTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// var xml = PublishedContentXml.BaseWebTestXml(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// GlobalSettings.HideTopLevelNodeFromPath = true; +// } +// +// [TestCase(1046, "/")] +// [TestCase(1173, "/sub1/")] +// [TestCase(1174, "/sub1/sub2/")] +// [TestCase(1176, "/sub1/sub-3/")] +// [TestCase(1177, "/sub1/custom-sub-1/")] +// [TestCase(1178, "/sub1/custom-sub-2/")] +// [TestCase(1175, "/sub-2/")] +// [TestCase(1172, "/test-page/")] // not hidden because not first root +// public void Get_Url_Hiding_Top_Level(int nodeId, string niceUrlMatch) +// { +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); +// +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out var uriUtility); +// +// var result = urlProvider.GetUrl(nodeId); +// Assert.AreEqual(niceUrlMatch, result); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlProviderWithoutHideTopLevelNodeFromPathTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlProviderWithoutHideTopLevelNodeFromPathTests.cs index 7c8a1dc883..5e95281434 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlProviderWithoutHideTopLevelNodeFromPathTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlProviderWithoutHideTopLevelNodeFromPathTests.cs @@ -1,310 +1,311 @@ -using System.Collections.Generic; -using System.Linq; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Builders.Extensions; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class UrlProviderWithoutHideTopLevelNodeFromPathTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - GlobalSettings.HideTopLevelNodeFromPath = false; - } - - private const string CacheKeyPrefix = "NuCache.ContentCache.RouteByContent"; - - private void PopulateCache(string culture = "fr-FR") - { - var dataTypes = GetDefaultDataTypes(); - var propertyDataTypes = new Dictionary - { - // we only have one data type for this test which will be resolved with string empty. - [string.Empty] = dataTypes[0], - }; - var contentType1 = new ContentType(ShortStringHelper, -1); - - var rootData = new ContentDataBuilder() - .WithName("Page" + Guid.NewGuid()) - .WithCultureInfos(new Dictionary - { - [culture] = new() { Name = "root", IsDraft = true, Date = DateTime.Now, UrlSegment = "root" }, - }) - .Build(ShortStringHelper, propertyDataTypes, contentType1, "alias"); - - var root = ContentNodeKitBuilder.CreateWithContent( - contentType1.Id, - 9876, - "-1,9876", - draftData: rootData, - publishedData: rootData); - - var parentData = new ContentDataBuilder() - .WithName("Page" + Guid.NewGuid()) - .WithCultureInfos(new Dictionary - { - [culture] = new() { Name = "home", IsDraft = true, Date = DateTime.Now, UrlSegment = "home" }, - }) - .Build(); - - var parent = ContentNodeKitBuilder.CreateWithContent( - contentType1.Id, - 5432, - "-1,9876,5432", - parentContentId: 9876, - draftData: parentData, - publishedData: parentData); - - var contentData = new ContentDataBuilder() - .WithName("Page" + Guid.NewGuid()) - .WithCultureInfos(new Dictionary - { - [culture] = new() { Name = "name-fr2", IsDraft = true, Date = DateTime.Now, UrlSegment = "test-fr" }, - }) - .Build(); - - var content = ContentNodeKitBuilder.CreateWithContent( - contentType1.Id, - 1234, - "-1,9876,5432,1234", - parentContentId: 5432, - draftData: contentData, - publishedData: contentData); - - InitializedCache(new[] { root, parent, content }, new[] { contentType1 }, dataTypes); - } - - private void SetDomains1() - { - var domainService = Mock.Get(DomainService); - - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("http://example.us/") { Id = 1, RootContentId = 9876, LanguageIsoCode = "en-US" }, - new UmbracoDomain("http://example.fr/") { Id = 2, RootContentId = 9876, LanguageIsoCode = "fr-FR" }, - }); - } - - /// - /// This checks that when we retrieve a NiceUrl for multiple items that there are no issues with cache overlap - /// and that they are all cached correctly. - /// - [Test] - public void Ensure_Cache_Is_Correct() - { - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = false }; - - var xml = PublishedContentXml.BaseWebTestXml(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - - var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - var samples = new Dictionary - { - { 1046, "/home" }, - { 1173, "/home/sub1" }, - { 1174, "/home/sub1/sub2" }, - { 1176, "/home/sub1/sub-3" }, - { 1177, "/home/sub1/custom-sub-1" }, - { 1178, "/home/sub1/custom-sub-2" }, - { 1175, "/home/sub-2" }, - { 1172, "/test-page" }, - }; - - foreach (var sample in samples) - { - var result = urlProvider.GetUrl(sample.Key); - Assert.AreEqual(sample.Value, result); - } - - var randomSample = new KeyValuePair(1177, "/home/sub1/custom-sub-1"); - for (var i = 0; i < 5; i++) - { - var result = urlProvider.GetUrl(randomSample.Key); - Assert.AreEqual(randomSample.Value, result); - } - - var cache = (FastDictionaryAppCache)umbracoContext.PublishedSnapshot.ElementsCache; - var cachedRoutes = cache.Keys.Where(x => x.StartsWith(CacheKeyPrefix)).ToList(); - Assert.AreEqual(8, cachedRoutes.Count); - - foreach (var sample in samples) - { - var cacheKey = $"{CacheKeyPrefix}[P:{sample.Key}]"; - var found = (string)cache.Get(cacheKey); - Assert.IsNotNull(found); - Assert.AreEqual(sample.Value, found); - } - } - - [TestCase(1046, "/home/")] - [TestCase(1173, "/home/sub1/")] - [TestCase(1174, "/home/sub1/sub2/")] - [TestCase(1176, "/home/sub1/sub-3/")] - [TestCase(1177, "/home/sub1/custom-sub-1/")] - [TestCase(1178, "/home/sub1/custom-sub-2/")] - [TestCase(1175, "/home/sub-2/")] - [TestCase(1172, "/test-page/")] - public void Get_Url_Not_Hiding_Top_Level(int nodeId, string niceUrlMatch) - { - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - - var xml = PublishedContentXml.BaseWebTestXml(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - - var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - var result = urlProvider.GetUrl(nodeId); - Assert.AreEqual(niceUrlMatch, result); - } - - [Test] - [TestCase("fr-FR", ExpectedResult = "#")] // Non default cultures cannot return urls - [TestCase("en-US", ExpectedResult = "/root/home/test-fr/")] // Default culture can return urls - public string Get_Url_For_Culture_Variant_Without_Domains_Non_Current_Url(string culture) - { - const string currentUri = "http://example.us/test"; - - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - - PopulateCache(culture); - - var umbracoContextAccessor = GetUmbracoContextAccessor(currentUri); - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - // even though we are asking for a specific culture URL, there are no domains assigned so all that can be returned is a normal relative URL. - var url = urlProvider.GetUrl(1234, culture: culture); - - return url; - } - - /// - /// This tests DefaultUrlProvider.GetUrl with a specific culture when the current URL is the culture specific domain - /// - [Test] - public void Get_Url_For_Culture_Variant_With_Current_Url() - { - const string currentUri = "http://example.fr/test"; - - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - - PopulateCache(); - - SetDomains1(); - - var umbracoContextAccessor = GetUmbracoContextAccessor(currentUri); - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - var url = urlProvider.GetUrl(1234, culture: "fr-FR"); - - Assert.AreEqual("/home/test-fr/", url); - } - - /// - /// This tests DefaultUrlProvider.GetUrl with a specific culture when the current URL is not the culture specific - /// domain - /// - [Test] - public void Get_Url_For_Culture_Variant_Non_Current_Url() - { - const string currentUri = "http://example.us/test"; - - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - - PopulateCache(); - - SetDomains1(); - - var umbracoContextAccessor = GetUmbracoContextAccessor(currentUri); - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - var url = urlProvider.GetUrl(1234, culture: "fr-FR"); - - // the current uri is not the culture specific domain we want, so the result is an absolute path to the culture specific domain - Assert.AreEqual("http://example.fr/home/test-fr/", url); - } - - [Test] - public void Get_Url_Relative_Or_Absolute() - { - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - - var xml = PublishedContentXml.BaseWebTestXml(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - - var umbracoContextAccessor = GetUmbracoContextAccessor("http://example.com/test"); - - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - Assert.AreEqual("/home/sub1/custom-sub-1/", urlProvider.GetUrl(1177)); - - urlProvider.Mode = UrlMode.Absolute; - Assert.AreEqual("http://example.com/home/sub1/custom-sub-1/", urlProvider.GetUrl(1177)); - } - - [Test] - public void Get_Url_Unpublished() - { - var requestHandlerSettings = new RequestHandlerSettings(); - - var xml = PublishedContentXml.BaseWebTestXml(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - - var umbracoContextAccessor = GetUmbracoContextAccessor("http://example.com/test"); - - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - // mock the Umbraco settings that we need - Assert.AreEqual("#", urlProvider.GetUrl(999999)); - - urlProvider.Mode = UrlMode.Absolute; - - Assert.AreEqual("#", urlProvider.GetUrl(999999)); - } -} +// using System.Collections.Generic; +// using System.Linq; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Cache; +// using Umbraco.Cms.Core.Configuration.Models; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +// using Umbraco.Cms.Tests.Common.Builders; +// using Umbraco.Cms.Tests.Common.Builders.Extensions; +// using Umbraco.Cms.Tests.Common.Published; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class UrlProviderWithoutHideTopLevelNodeFromPathTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// GlobalSettings.HideTopLevelNodeFromPath = false; +// } +// +// private const string CacheKeyPrefix = "NuCache.ContentCache.RouteByContent"; +// +// private void PopulateCache(string culture = "fr-FR") +// { +// var dataTypes = GetDefaultDataTypes(); +// var propertyDataTypes = new Dictionary +// { +// // we only have one data type for this test which will be resolved with string empty. +// [string.Empty] = dataTypes[0], +// }; +// var contentType1 = new ContentType(ShortStringHelper, -1); +// +// var rootData = new ContentDataBuilder() +// .WithName("Page" + Guid.NewGuid()) +// .WithCultureInfos(new Dictionary +// { +// [culture] = new() { Name = "root", IsDraft = true, Date = DateTime.Now, UrlSegment = "root" }, +// }) +// .Build(ShortStringHelper, propertyDataTypes, contentType1, "alias"); +// +// var root = ContentNodeKitBuilder.CreateWithContent( +// contentType1.Id, +// 9876, +// "-1,9876", +// draftData: rootData, +// publishedData: rootData); +// +// var parentData = new ContentDataBuilder() +// .WithName("Page" + Guid.NewGuid()) +// .WithCultureInfos(new Dictionary +// { +// [culture] = new() { Name = "home", IsDraft = true, Date = DateTime.Now, UrlSegment = "home" }, +// }) +// .Build(); +// +// var parent = ContentNodeKitBuilder.CreateWithContent( +// contentType1.Id, +// 5432, +// "-1,9876,5432", +// parentContentId: 9876, +// draftData: parentData, +// publishedData: parentData); +// +// var contentData = new ContentDataBuilder() +// .WithName("Page" + Guid.NewGuid()) +// .WithCultureInfos(new Dictionary +// { +// [culture] = new() { Name = "name-fr2", IsDraft = true, Date = DateTime.Now, UrlSegment = "test-fr" }, +// }) +// .Build(); +// +// var content = ContentNodeKitBuilder.CreateWithContent( +// contentType1.Id, +// 1234, +// "-1,9876,5432,1234", +// parentContentId: 5432, +// draftData: contentData, +// publishedData: contentData); +// +// InitializedCache(new[] { root, parent, content }, new[] { contentType1 }, dataTypes); +// } +// +// private void SetDomains1() +// { +// var domainService = Mock.Get(DomainService); +// +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("http://example.us/") { Id = 1, RootContentId = 9876, LanguageIsoCode = "en-US" }, +// new UmbracoDomain("http://example.fr/") { Id = 2, RootContentId = 9876, LanguageIsoCode = "fr-FR" }, +// }); +// } +// +// /// +// /// This checks that when we retrieve a NiceUrl for multiple items that there are no issues with cache overlap +// /// and that they are all cached correctly. +// /// +// [Test] +// public void Ensure_Cache_Is_Correct() +// { +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = false }; +// +// var xml = PublishedContentXml.BaseWebTestXml(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// var samples = new Dictionary +// { +// { 1046, "/home" }, +// { 1173, "/home/sub1" }, +// { 1174, "/home/sub1/sub2" }, +// { 1176, "/home/sub1/sub-3" }, +// { 1177, "/home/sub1/custom-sub-1" }, +// { 1178, "/home/sub1/custom-sub-2" }, +// { 1175, "/home/sub-2" }, +// { 1172, "/test-page" }, +// }; +// +// foreach (var sample in samples) +// { +// var result = urlProvider.GetUrl(sample.Key); +// Assert.AreEqual(sample.Value, result); +// } +// +// var randomSample = new KeyValuePair(1177, "/home/sub1/custom-sub-1"); +// for (var i = 0; i < 5; i++) +// { +// var result = urlProvider.GetUrl(randomSample.Key); +// Assert.AreEqual(randomSample.Value, result); +// } +// +// var cache = (FastDictionaryAppCache)umbracoContext.PublishedSnapshot.ElementsCache; +// var cachedRoutes = cache.Keys.Where(x => x.StartsWith(CacheKeyPrefix)).ToList(); +// Assert.AreEqual(8, cachedRoutes.Count); +// +// foreach (var sample in samples) +// { +// var cacheKey = $"{CacheKeyPrefix}[P:{sample.Key}]"; +// var found = (string)cache.Get(cacheKey); +// Assert.IsNotNull(found); +// Assert.AreEqual(sample.Value, found); +// } +// } +// +// [TestCase(1046, "/home/")] +// [TestCase(1173, "/home/sub1/")] +// [TestCase(1174, "/home/sub1/sub2/")] +// [TestCase(1176, "/home/sub1/sub-3/")] +// [TestCase(1177, "/home/sub1/custom-sub-1/")] +// [TestCase(1178, "/home/sub1/custom-sub-2/")] +// [TestCase(1175, "/home/sub-2/")] +// [TestCase(1172, "/test-page/")] +// public void Get_Url_Not_Hiding_Top_Level(int nodeId, string niceUrlMatch) +// { +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// +// var xml = PublishedContentXml.BaseWebTestXml(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// var result = urlProvider.GetUrl(nodeId); +// Assert.AreEqual(niceUrlMatch, result); +// } +// +// [Test] +// [TestCase("fr-FR", ExpectedResult = "#")] // Non default cultures cannot return urls +// [TestCase("en-US", ExpectedResult = "/root/home/test-fr/")] // Default culture can return urls +// public string Get_Url_For_Culture_Variant_Without_Domains_Non_Current_Url(string culture) +// { +// const string currentUri = "http://example.us/test"; +// +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// +// PopulateCache(culture); +// +// var umbracoContextAccessor = GetUmbracoContextAccessor(currentUri); +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// // even though we are asking for a specific culture URL, there are no domains assigned so all that can be returned is a normal relative URL. +// var url = urlProvider.GetUrl(1234, culture: culture); +// +// return url; +// } +// +// /// +// /// This tests DefaultUrlProvider.GetUrl with a specific culture when the current URL is the culture specific domain +// /// +// [Test] +// public void Get_Url_For_Culture_Variant_With_Current_Url() +// { +// const string currentUri = "http://example.fr/test"; +// +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// +// PopulateCache(); +// +// SetDomains1(); +// +// var umbracoContextAccessor = GetUmbracoContextAccessor(currentUri); +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// var url = urlProvider.GetUrl(1234, culture: "fr-FR"); +// +// Assert.AreEqual("/home/test-fr/", url); +// } +// +// /// +// /// This tests DefaultUrlProvider.GetUrl with a specific culture when the current URL is not the culture specific +// /// domain +// /// +// [Test] +// public void Get_Url_For_Culture_Variant_Non_Current_Url() +// { +// const string currentUri = "http://example.us/test"; +// +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// +// PopulateCache(); +// +// SetDomains1(); +// +// var umbracoContextAccessor = GetUmbracoContextAccessor(currentUri); +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// var url = urlProvider.GetUrl(1234, culture: "fr-FR"); +// +// // the current uri is not the culture specific domain we want, so the result is an absolute path to the culture specific domain +// Assert.AreEqual("http://example.fr/home/test-fr/", url); +// } +// +// [Test] +// public void Get_Url_Relative_Or_Absolute() +// { +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// +// var xml = PublishedContentXml.BaseWebTestXml(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("http://example.com/test"); +// +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// Assert.AreEqual("/home/sub1/custom-sub-1/", urlProvider.GetUrl(1177)); +// +// urlProvider.Mode = UrlMode.Absolute; +// Assert.AreEqual("http://example.com/home/sub1/custom-sub-1/", urlProvider.GetUrl(1177)); +// } +// +// [Test] +// public void Get_Url_Unpublished() +// { +// var requestHandlerSettings = new RequestHandlerSettings(); +// +// var xml = PublishedContentXml.BaseWebTestXml(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("http://example.com/test"); +// +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// // mock the Umbraco settings that we need +// Assert.AreEqual("#", urlProvider.GetUrl(999999)); +// +// urlProvider.Mode = UrlMode.Absolute; +// +// Assert.AreEqual("#", urlProvider.GetUrl(999999)); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlRoutingTestBase.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlRoutingTestBase.cs index ee4c32d58d..3431fe8bad 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlRoutingTestBase.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlRoutingTestBase.cs @@ -1,202 +1,203 @@ -using System.Collections.Generic; -using System.Linq; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public abstract class UrlRoutingTestBase : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - var xml = GetXmlContent(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - } - - // Sets up the mock domain service - protected override ServiceContext CreateServiceContext(IContentType[] contentTypes, IMediaType[] mediaTypes, IDataType[] dataTypes) - { - var serviceContext = base.CreateServiceContext(contentTypes, mediaTypes, dataTypes); - - // setup mock domain service - var domainService = Mock.Get(serviceContext.DomainService); - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("domain1.com/") - { - Id = 1, LanguageId = LangDeId, RootContentId = 1001, LanguageIsoCode = "de-DE", - }, - new UmbracoDomain("domain1.com/en") - { - Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", - }, - new UmbracoDomain("domain1.com/fr") - { - Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", - }, - }); - - return serviceContext; - } - - protected virtual string GetXmlContent(int templateId) - => @" - - -]> - - - - - - This is some content]]> - - - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - - -"; - - public const int LangDeId = 333; - public const int LangEngId = 334; - public const int LangFrId = 335; - public const int LangCzId = 336; - public const int LangNlId = 337; - public const int LangDkId = 338; -} +// using System.Collections.Generic; +// using System.Linq; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Services; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Published; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// +// // FIXME: Reintroduce if relevant +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// [TestFixture] +// public abstract class UrlRoutingTestBase : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// var xml = GetXmlContent(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// } +// +// // Sets up the mock domain service +// protected override ServiceContext CreateServiceContext(IContentType[] contentTypes, IMediaType[] mediaTypes, IDataType[] dataTypes) +// { +// var serviceContext = base.CreateServiceContext(contentTypes, mediaTypes, dataTypes); +// +// // setup mock domain service +// var domainService = Mock.Get(serviceContext.DomainService); +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("domain1.com/") +// { +// Id = 1, LanguageId = LangDeId, RootContentId = 1001, LanguageIsoCode = "de-DE", +// }, +// new UmbracoDomain("domain1.com/en") +// { +// Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", +// }, +// new UmbracoDomain("domain1.com/fr") +// { +// Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", +// }, +// }); +// +// return serviceContext; +// } +// +// protected virtual string GetXmlContent(int templateId) +// => @" +// +// +// ]> +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// "; +// +// public const int LangDeId = 333; +// public const int LangEngId = 334; +// public const int LangFrId = 335; +// public const int LangCzId = 336; +// public const int LangNlId = 337; +// public const int LangDkId = 338; +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsProviderWithDomainsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsProviderWithDomainsTests.cs index 70084e28c6..d35d804ed4 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsProviderWithDomainsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsProviderWithDomainsTests.cs @@ -1,483 +1,484 @@ -using System.Linq; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class UrlsProviderWithDomainsTests : UrlRoutingTestBase -{ - private const string CacheKeyPrefix = "NuCache.ContentCache.RouteByContent"; - - private void SetDomains1() - { - var domainService = Mock.Get(DomainService); - - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("domain1.com") - { - Id = 1, LanguageId = LangFrId, RootContentId = 1001, LanguageIsoCode = "fr-FR", SortOrder = 0, - }, - }); - } - - private void SetDomains2() - { - var domainService = Mock.Get(DomainService); - - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("http://domain1.com/foo") - { - Id = 1, LanguageId = LangFrId, RootContentId = 1001, LanguageIsoCode = "fr-FR", SortOrder = 0, - }, - }); - } - - private void SetDomains3() - { - var domainService = Mock.Get(DomainService); - - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("http://domain1.com/") - { - Id = 1, LanguageId = LangFrId, RootContentId = 10011, LanguageIsoCode = "fr-FR", SortOrder = 0, - }, - }); - } - - private void SetDomains4() - { - var domainService = Mock.Get(DomainService); - - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("http://domain1.com/") - { - Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US", SortOrder = 0, - }, - new UmbracoDomain("http://domain1.com/en") - { - Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", SortOrder = 0, - }, - new UmbracoDomain("http://domain1.com/fr") - { - Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", SortOrder = 0, - }, - new UmbracoDomain("http://domain3.com/") - { - Id = 4, LanguageId = LangEngId, RootContentId = 1003, LanguageIsoCode = "en-US", SortOrder = 0, - }, - new UmbracoDomain("http://domain3.com/en") - { - Id = 5, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US", SortOrder = 0, - }, - new UmbracoDomain("http://domain3.com/fr") - { - Id = 6, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR", SortOrder = 0, - }, - }); - } - - private void SetDomains5() - { - var domainService = Mock.Get(DomainService); - - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("http://domain1.com/en") - { - Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", SortOrder = 0, - }, - new UmbracoDomain("http://domain1a.com/en") - { - Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", SortOrder = 1, - }, - new UmbracoDomain("http://domain1b.com/en") - { - Id = 3, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", SortOrder = 2, - }, - new UmbracoDomain("http://domain1.com/fr") - { - Id = 4, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", SortOrder = 0, - }, - new UmbracoDomain("http://domain1a.com/fr") - { - Id = 5, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", SortOrder = 1, - }, - new UmbracoDomain("http://domain1b.com/fr") - { - Id = 6, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", SortOrder = 2, - }, - new UmbracoDomain("http://domain3.com/en") - { - Id = 7, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US", SortOrder = 0, - }, - new UmbracoDomain("http://domain3.com/fr") - { - Id = 8, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR", SortOrder = 0, - }, - }); - } - - protected override string GetXmlContent(int templateId) - => @" - - -]> - - - - - This is some content]]> - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - - -"; - - // with one simple domain "domain1.com" - // basic tests - [TestCase(1001, "http://domain1.com", false, "/")] - [TestCase(10011, "http://domain1.com", false, "/1001-1/")] - [TestCase(1002, "http://domain1.com", false, "/1002/")] - - // absolute tests - [TestCase(1001, "http://domain1.com", true, "http://domain1.com/")] - [TestCase(10011, "http://domain1.com", true, "http://domain1.com/1001-1/")] - - // different current tests - [TestCase(1001, "http://domain2.com", false, "http://domain1.com/")] - [TestCase(10011, "http://domain2.com", false, "http://domain1.com/1001-1/")] - [TestCase(1001, "https://domain1.com", false, "/")] - [TestCase(10011, "https://domain1.com", false, "/1001-1/")] - public void Get_Url_SimpleDomain(int nodeId, string currentUrl, bool absolute, string expected) - { - SetDomains1(); - - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - GlobalSettings.HideTopLevelNodeFromPath = false; - - var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); - - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - var currentUri = new Uri(currentUrl); - var mode = absolute ? UrlMode.Absolute : UrlMode.Auto; - var result = urlProvider.GetUrl(nodeId, mode, current: currentUri); - Assert.AreEqual(expected, result); - } - - // with one complete domain "http://domain1.com/foo" - // basic tests - [TestCase(1001, "http://domain1.com", false, "/foo/")] - [TestCase(10011, "http://domain1.com", false, "/foo/1001-1/")] - [TestCase(1002, "http://domain1.com", false, "/1002/")] - - // absolute tests - [TestCase(1001, "http://domain1.com", true, "http://domain1.com/foo/")] - [TestCase(10011, "http://domain1.com", true, "http://domain1.com/foo/1001-1/")] - - // different current tests - [TestCase(1001, "http://domain2.com", false, "http://domain1.com/foo/")] - [TestCase(10011, "http://domain2.com", false, "http://domain1.com/foo/1001-1/")] - [TestCase(1001, "https://domain1.com", false, "http://domain1.com/foo/")] - [TestCase(10011, "https://domain1.com", false, "http://domain1.com/foo/1001-1/")] - public void Get_Url_SimpleWithSchemeAndPath(int nodeId, string currentUrl, bool absolute, string expected) - { - SetDomains2(); - - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - GlobalSettings.HideTopLevelNodeFromPath = false; - - var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); - - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - var currentUri = new Uri(currentUrl); - var mode = absolute ? UrlMode.Absolute : UrlMode.Auto; - var result = urlProvider.GetUrl(nodeId, mode, current: currentUri); - Assert.AreEqual(expected, result); - } - - // with one domain, not at root - [TestCase(1001, "http://domain1.com", false, "/1001/")] - [TestCase(10011, "http://domain1.com", false, "/")] - [TestCase(100111, "http://domain1.com", false, "/1001-1-1/")] - [TestCase(1002, "http://domain1.com", false, "/1002/")] - public void Get_Url_DeepDomain(int nodeId, string currentUrl, bool absolute, string expected) - { - SetDomains3(); - - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - GlobalSettings.HideTopLevelNodeFromPath = false; - - var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); - - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - var currentUri = new Uri(currentUrl); - var mode = absolute ? UrlMode.Absolute : UrlMode.Auto; - var result = urlProvider.GetUrl(nodeId, mode, current: currentUri); - Assert.AreEqual(expected, result); - } - - // with nested domains - [TestCase(1001, "http://domain1.com", false, "/")] - [TestCase(10011, "http://domain1.com", false, "/en/")] - [TestCase(100111, "http://domain1.com", false, "/en/1001-1-1/")] - [TestCase(10012, "http://domain1.com", false, "/fr/")] - [TestCase(100121, "http://domain1.com", false, "/fr/1001-2-1/")] - [TestCase(10013, "http://domain1.com", false, "/1001-3/")] - [TestCase(1002, "http://domain1.com", false, "/1002/")] - [TestCase(1003, "http://domain3.com", false, "/")] - [TestCase(10031, "http://domain3.com", false, "/en/")] - [TestCase(100321, "http://domain3.com", false, "/fr/1003-2-1/")] - public void Get_Url_NestedDomains(int nodeId, string currentUrl, bool absolute, string expected) - { - SetDomains4(); - - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - GlobalSettings.HideTopLevelNodeFromPath = false; - - var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); - - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - var currentUri = new Uri(currentUrl); - var mode = absolute ? UrlMode.Absolute : UrlMode.Auto; - var result = urlProvider.GetUrl(nodeId, mode, current: currentUri); - Assert.AreEqual(expected, result); - } - - [Test] - public void Get_Url_DomainsAndCache() - { - SetDomains4(); - - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - GlobalSettings.HideTopLevelNodeFromPath = false; - - var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - urlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain1.com")); - urlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain1.com")); - urlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain1.com")); - urlProvider.GetUrl(10012, UrlMode.Auto, current: new Uri("http://domain1.com")); - urlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain1.com")); - urlProvider.GetUrl(10013, UrlMode.Auto, current: new Uri("http://domain1.com")); - urlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain1.com")); - urlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain2.com")); - urlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain2.com")); - urlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain2.com")); - urlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain2.com")); - - var cache = (FastDictionaryAppCache)umbracoContext.PublishedSnapshot.ElementsCache; - var cachedRoutes = cache.Keys.Where(x => x.StartsWith(CacheKeyPrefix)).ToList(); - Assert.AreEqual(7, cachedRoutes.Count); - - // var cachedIds = cache.RoutesCache.GetCachedIds(); - // Assert.AreEqual(0, cachedIds.Count); - CheckRoute(cache, 1001, "1001/"); - CheckRoute(cache, 10011, "10011/"); - CheckRoute(cache, 100111, "10011/1001-1-1"); - CheckRoute(cache, 10012, "10012/"); - CheckRoute(cache, 100121, "10012/1001-2-1"); - CheckRoute(cache, 10013, "1001/1001-3"); - CheckRoute(cache, 1002, "/1002"); - - // use the cache - Assert.AreEqual("/", urlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain1.com"))); - Assert.AreEqual("/en/", urlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain1.com"))); - Assert.AreEqual("/en/1001-1-1/", urlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain1.com"))); - Assert.AreEqual("/fr/", urlProvider.GetUrl(10012, UrlMode.Auto, current: new Uri("http://domain1.com"))); - Assert.AreEqual("/fr/1001-2-1/", urlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain1.com"))); - Assert.AreEqual("/1001-3/", urlProvider.GetUrl(10013, UrlMode.Auto, current: new Uri("http://domain1.com"))); - Assert.AreEqual("/1002/", urlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain1.com"))); - - Assert.AreEqual("http://domain1.com/fr/1001-2-1/", urlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain2.com"))); - } - - private static void CheckRoute(FastDictionaryAppCache routes, int id, string route) - { - var cacheKey = $"{CacheKeyPrefix}[P:{id}]"; - var found = (string)routes.Get(cacheKey); - Assert.IsNotNull(found); - Assert.AreEqual(route, found); - } - - [Test] - public void Get_Url_Relative_Or_Absolute() - { - SetDomains4(); - - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - GlobalSettings.HideTopLevelNodeFromPath = false; - - var umbracoContextAccessor = GetUmbracoContextAccessor("http://domain1.com/test"); - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - Assert.AreEqual("/en/1001-1-1/", urlProvider.GetUrl(100111)); - Assert.AreEqual("http://domain3.com/en/1003-1-1/", urlProvider.GetUrl(100311)); - - urlProvider.Mode = UrlMode.Absolute; - - Assert.AreEqual("http://domain1.com/en/1001-1-1/", urlProvider.GetUrl(100111)); - Assert.AreEqual("http://domain3.com/en/1003-1-1/", urlProvider.GetUrl(100311)); - } - - [Test] - public void Get_Url_Alternate() - { - SetDomains5(); - - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - GlobalSettings.HideTopLevelNodeFromPath = false; - - var umbracoContextAccessor = GetUmbracoContextAccessor("http://domain1.com/en/test"); - var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); - - var url = urlProvider.GetUrl(100111, UrlMode.Absolute); - Assert.AreEqual("http://domain1.com/en/1001-1-1/", url); - - var result = urlProvider.GetOtherUrls(100111).ToArray(); - - foreach (var x in result) - { - Console.WriteLine(x); - } - - Assert.AreEqual(2, result.Length); - Assert.AreEqual(result[0].Text, "http://domain1a.com/en/1001-1-1/"); - Assert.AreEqual(result[1].Text, "http://domain1b.com/en/1001-1-1/"); - } -} +// using System.Linq; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Cache; +// using Umbraco.Cms.Core.Configuration.Models; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class UrlsProviderWithDomainsTests : UrlRoutingTestBase +// { +// private const string CacheKeyPrefix = "NuCache.ContentCache.RouteByContent"; +// +// private void SetDomains1() +// { +// var domainService = Mock.Get(DomainService); +// +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("domain1.com") +// { +// Id = 1, LanguageId = LangFrId, RootContentId = 1001, LanguageIsoCode = "fr-FR", SortOrder = 0, +// }, +// }); +// } +// +// private void SetDomains2() +// { +// var domainService = Mock.Get(DomainService); +// +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("http://domain1.com/foo") +// { +// Id = 1, LanguageId = LangFrId, RootContentId = 1001, LanguageIsoCode = "fr-FR", SortOrder = 0, +// }, +// }); +// } +// +// private void SetDomains3() +// { +// var domainService = Mock.Get(DomainService); +// +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("http://domain1.com/") +// { +// Id = 1, LanguageId = LangFrId, RootContentId = 10011, LanguageIsoCode = "fr-FR", SortOrder = 0, +// }, +// }); +// } +// +// private void SetDomains4() +// { +// var domainService = Mock.Get(DomainService); +// +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("http://domain1.com/") +// { +// Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US", SortOrder = 0, +// }, +// new UmbracoDomain("http://domain1.com/en") +// { +// Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", SortOrder = 0, +// }, +// new UmbracoDomain("http://domain1.com/fr") +// { +// Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", SortOrder = 0, +// }, +// new UmbracoDomain("http://domain3.com/") +// { +// Id = 4, LanguageId = LangEngId, RootContentId = 1003, LanguageIsoCode = "en-US", SortOrder = 0, +// }, +// new UmbracoDomain("http://domain3.com/en") +// { +// Id = 5, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US", SortOrder = 0, +// }, +// new UmbracoDomain("http://domain3.com/fr") +// { +// Id = 6, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR", SortOrder = 0, +// }, +// }); +// } +// +// private void SetDomains5() +// { +// var domainService = Mock.Get(DomainService); +// +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("http://domain1.com/en") +// { +// Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", SortOrder = 0, +// }, +// new UmbracoDomain("http://domain1a.com/en") +// { +// Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", SortOrder = 1, +// }, +// new UmbracoDomain("http://domain1b.com/en") +// { +// Id = 3, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", SortOrder = 2, +// }, +// new UmbracoDomain("http://domain1.com/fr") +// { +// Id = 4, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", SortOrder = 0, +// }, +// new UmbracoDomain("http://domain1a.com/fr") +// { +// Id = 5, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", SortOrder = 1, +// }, +// new UmbracoDomain("http://domain1b.com/fr") +// { +// Id = 6, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", SortOrder = 2, +// }, +// new UmbracoDomain("http://domain3.com/en") +// { +// Id = 7, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US", SortOrder = 0, +// }, +// new UmbracoDomain("http://domain3.com/fr") +// { +// Id = 8, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR", SortOrder = 0, +// }, +// }); +// } +// +// protected override string GetXmlContent(int templateId) +// => @" +// +// +// ]> +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// "; +// +// // with one simple domain "domain1.com" +// // basic tests +// [TestCase(1001, "http://domain1.com", false, "/")] +// [TestCase(10011, "http://domain1.com", false, "/1001-1/")] +// [TestCase(1002, "http://domain1.com", false, "/1002/")] +// +// // absolute tests +// [TestCase(1001, "http://domain1.com", true, "http://domain1.com/")] +// [TestCase(10011, "http://domain1.com", true, "http://domain1.com/1001-1/")] +// +// // different current tests +// [TestCase(1001, "http://domain2.com", false, "http://domain1.com/")] +// [TestCase(10011, "http://domain2.com", false, "http://domain1.com/1001-1/")] +// [TestCase(1001, "https://domain1.com", false, "/")] +// [TestCase(10011, "https://domain1.com", false, "/1001-1/")] +// public void Get_Url_SimpleDomain(int nodeId, string currentUrl, bool absolute, string expected) +// { +// SetDomains1(); +// +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); +// +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// var currentUri = new Uri(currentUrl); +// var mode = absolute ? UrlMode.Absolute : UrlMode.Auto; +// var result = urlProvider.GetUrl(nodeId, mode, current: currentUri); +// Assert.AreEqual(expected, result); +// } +// +// // with one complete domain "http://domain1.com/foo" +// // basic tests +// [TestCase(1001, "http://domain1.com", false, "/foo/")] +// [TestCase(10011, "http://domain1.com", false, "/foo/1001-1/")] +// [TestCase(1002, "http://domain1.com", false, "/1002/")] +// +// // absolute tests +// [TestCase(1001, "http://domain1.com", true, "http://domain1.com/foo/")] +// [TestCase(10011, "http://domain1.com", true, "http://domain1.com/foo/1001-1/")] +// +// // different current tests +// [TestCase(1001, "http://domain2.com", false, "http://domain1.com/foo/")] +// [TestCase(10011, "http://domain2.com", false, "http://domain1.com/foo/1001-1/")] +// [TestCase(1001, "https://domain1.com", false, "http://domain1.com/foo/")] +// [TestCase(10011, "https://domain1.com", false, "http://domain1.com/foo/1001-1/")] +// public void Get_Url_SimpleWithSchemeAndPath(int nodeId, string currentUrl, bool absolute, string expected) +// { +// SetDomains2(); +// +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); +// +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// var currentUri = new Uri(currentUrl); +// var mode = absolute ? UrlMode.Absolute : UrlMode.Auto; +// var result = urlProvider.GetUrl(nodeId, mode, current: currentUri); +// Assert.AreEqual(expected, result); +// } +// +// // with one domain, not at root +// [TestCase(1001, "http://domain1.com", false, "/1001/")] +// [TestCase(10011, "http://domain1.com", false, "/")] +// [TestCase(100111, "http://domain1.com", false, "/1001-1-1/")] +// [TestCase(1002, "http://domain1.com", false, "/1002/")] +// public void Get_Url_DeepDomain(int nodeId, string currentUrl, bool absolute, string expected) +// { +// SetDomains3(); +// +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); +// +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// var currentUri = new Uri(currentUrl); +// var mode = absolute ? UrlMode.Absolute : UrlMode.Auto; +// var result = urlProvider.GetUrl(nodeId, mode, current: currentUri); +// Assert.AreEqual(expected, result); +// } +// +// // with nested domains +// [TestCase(1001, "http://domain1.com", false, "/")] +// [TestCase(10011, "http://domain1.com", false, "/en/")] +// [TestCase(100111, "http://domain1.com", false, "/en/1001-1-1/")] +// [TestCase(10012, "http://domain1.com", false, "/fr/")] +// [TestCase(100121, "http://domain1.com", false, "/fr/1001-2-1/")] +// [TestCase(10013, "http://domain1.com", false, "/1001-3/")] +// [TestCase(1002, "http://domain1.com", false, "/1002/")] +// [TestCase(1003, "http://domain3.com", false, "/")] +// [TestCase(10031, "http://domain3.com", false, "/en/")] +// [TestCase(100321, "http://domain3.com", false, "/fr/1003-2-1/")] +// public void Get_Url_NestedDomains(int nodeId, string currentUrl, bool absolute, string expected) +// { +// SetDomains4(); +// +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); +// +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// var currentUri = new Uri(currentUrl); +// var mode = absolute ? UrlMode.Absolute : UrlMode.Auto; +// var result = urlProvider.GetUrl(nodeId, mode, current: currentUri); +// Assert.AreEqual(expected, result); +// } +// +// [Test] +// public void Get_Url_DomainsAndCache() +// { +// SetDomains4(); +// +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("/test"); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// urlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain1.com")); +// urlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain1.com")); +// urlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain1.com")); +// urlProvider.GetUrl(10012, UrlMode.Auto, current: new Uri("http://domain1.com")); +// urlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain1.com")); +// urlProvider.GetUrl(10013, UrlMode.Auto, current: new Uri("http://domain1.com")); +// urlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain1.com")); +// urlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain2.com")); +// urlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain2.com")); +// urlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain2.com")); +// urlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain2.com")); +// +// var cache = (FastDictionaryAppCache)umbracoContext.PublishedSnapshot.ElementsCache; +// var cachedRoutes = cache.Keys.Where(x => x.StartsWith(CacheKeyPrefix)).ToList(); +// Assert.AreEqual(7, cachedRoutes.Count); +// +// // var cachedIds = cache.RoutesCache.GetCachedIds(); +// // Assert.AreEqual(0, cachedIds.Count); +// CheckRoute(cache, 1001, "1001/"); +// CheckRoute(cache, 10011, "10011/"); +// CheckRoute(cache, 100111, "10011/1001-1-1"); +// CheckRoute(cache, 10012, "10012/"); +// CheckRoute(cache, 100121, "10012/1001-2-1"); +// CheckRoute(cache, 10013, "1001/1001-3"); +// CheckRoute(cache, 1002, "/1002"); +// +// // use the cache +// Assert.AreEqual("/", urlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain1.com"))); +// Assert.AreEqual("/en/", urlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain1.com"))); +// Assert.AreEqual("/en/1001-1-1/", urlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain1.com"))); +// Assert.AreEqual("/fr/", urlProvider.GetUrl(10012, UrlMode.Auto, current: new Uri("http://domain1.com"))); +// Assert.AreEqual("/fr/1001-2-1/", urlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain1.com"))); +// Assert.AreEqual("/1001-3/", urlProvider.GetUrl(10013, UrlMode.Auto, current: new Uri("http://domain1.com"))); +// Assert.AreEqual("/1002/", urlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain1.com"))); +// +// Assert.AreEqual("http://domain1.com/fr/1001-2-1/", urlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain2.com"))); +// } +// +// private static void CheckRoute(FastDictionaryAppCache routes, int id, string route) +// { +// var cacheKey = $"{CacheKeyPrefix}[P:{id}]"; +// var found = (string)routes.Get(cacheKey); +// Assert.IsNotNull(found); +// Assert.AreEqual(route, found); +// } +// +// [Test] +// public void Get_Url_Relative_Or_Absolute() +// { +// SetDomains4(); +// +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("http://domain1.com/test"); +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// Assert.AreEqual("/en/1001-1-1/", urlProvider.GetUrl(100111)); +// Assert.AreEqual("http://domain3.com/en/1003-1-1/", urlProvider.GetUrl(100311)); +// +// urlProvider.Mode = UrlMode.Absolute; +// +// Assert.AreEqual("http://domain1.com/en/1001-1-1/", urlProvider.GetUrl(100111)); +// Assert.AreEqual("http://domain3.com/en/1003-1-1/", urlProvider.GetUrl(100311)); +// } +// +// [Test] +// public void Get_Url_Alternate() +// { +// SetDomains5(); +// +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var umbracoContextAccessor = GetUmbracoContextAccessor("http://domain1.com/en/test"); +// var urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out _); +// +// var url = urlProvider.GetUrl(100111, UrlMode.Absolute); +// Assert.AreEqual("http://domain1.com/en/1001-1-1/", url); +// +// var result = urlProvider.GetOtherUrls(100111).ToArray(); +// +// foreach (var x in result) +// { +// Console.WriteLine(x); +// } +// +// Assert.AreEqual(2, result.Length); +// Assert.AreEqual(result[0].Text, "http://domain1a.com/en/1001-1-1/"); +// Assert.AreEqual(result[1].Text, "http://domain1b.com/en/1001-1-1/"); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsWithNestedDomains.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsWithNestedDomains.cs index d0536640e2..ccb765c926 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsWithNestedDomains.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsWithNestedDomains.cs @@ -1,240 +1,241 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Tests.Common; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; - -[TestFixture] -public class UrlsWithNestedDomains : UrlRoutingTestBase -{ - // in the case of nested domains more than 1 URL may resolve to a document - // but only one route can be cached - the 'canonical' route ie the route - // using the closest domain to the node - here we test that if we request - // a non-canonical route, it is not cached / the cache is not polluted - [Test] - public async Task DoNotPolluteCache() - { - var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; - GlobalSettings.HideTopLevelNodeFromPath = false; - - SetDomains1(); - - const string url = "http://domain1.com/1001-1/1001-1-1"; - - // get the nice URL for 100111 - var umbracoContextAccessor = GetUmbracoContextAccessor(url); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - - var urlProvider = new DefaultUrlProvider( - Mock.Of>(x => x.CurrentValue == requestHandlerSettings), - Mock.Of>(), - new SiteDomainMapper(), - umbracoContextAccessor, - new UriUtility(Mock.Of()), - Mock.Of()); - var publishedUrlProvider = GetPublishedUrlProvider(umbracoContext, urlProvider); - - var absUrl = publishedUrlProvider.GetUrl(100111, UrlMode.Absolute); - Assert.AreEqual("http://domain2.com/1001-1-1/", absUrl); - - const string cacheKeyPrefix = "NuCache.ContentCache.RouteByContent"; - - // check that the proper route has been cached - var cache = (FastDictionaryAppCache)umbracoContext.PublishedSnapshot.ElementsCache; - - var cacheKey = $"{cacheKeyPrefix}[P:100111]"; - Assert.AreEqual("10011/1001-1-1", cache.Get(cacheKey)); - - // route a rogue URL - var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); - var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - - publishedRouter.FindAndSetDomain(frequest); - Assert.IsTrue(frequest.HasDomain()); - - // check that it's been routed - var lookup = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); - var result = await lookup.TryFindContent(frequest); - Assert.IsTrue(result); - Assert.AreEqual(100111, frequest.PublishedContent.Id); - - // has the cache been polluted? - Assert.AreEqual("10011/1001-1-1", cache.Get(cacheKey)); // no - - // what's the nice URL now? - Assert.AreEqual("http://domain2.com/1001-1-1/", publishedUrlProvider.GetUrl(100111)); // good - } - - private void SetDomains1() - { - var domainService = Mock.Get(DomainService); - - domainService.Setup(service => service.GetAll(It.IsAny())) - .Returns((bool incWildcards) => new[] - { - new UmbracoDomain("http://domain1.com/") - { - Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US", - }, - new UmbracoDomain("http://domain2.com/") - { - Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", - }, - }); - } - - private IPublishedUrlProvider GetPublishedUrlProvider(IUmbracoContext umbracoContext, DefaultUrlProvider urlProvider) - { - var webRoutingSettings = new WebRoutingSettings(); - return new UrlProvider( - new TestUmbracoContextAccessor(umbracoContext), - Options.Create(webRoutingSettings), - new UrlProviderCollection(() => new[] { urlProvider }), - new MediaUrlProviderCollection(() => Enumerable.Empty()), - Mock.Of()); - } - - protected override string GetXmlContent(int templateId) - => @" - - -]> - - - - - This is some content]]> - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - This is some content]]> - - - - - - - - - - - - - - - - -"; -} +// using System.Linq; +// using System.Threading.Tasks; +// using Microsoft.Extensions.Logging; +// using Microsoft.Extensions.Options; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Cache; +// using Umbraco.Cms.Core.Configuration.Models; +// using Umbraco.Cms.Core.Hosting; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Cms.Core.Services; +// using Umbraco.Cms.Core.Web; +// using Umbraco.Cms.Tests.Common; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class UrlsWithNestedDomains : UrlRoutingTestBase +// { +// // in the case of nested domains more than 1 URL may resolve to a document +// // but only one route can be cached - the 'canonical' route ie the route +// // using the closest domain to the node - here we test that if we request +// // a non-canonical route, it is not cached / the cache is not polluted +// [Test] +// public async Task DoNotPolluteCache() +// { +// var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true }; +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// SetDomains1(); +// +// const string url = "http://domain1.com/1001-1/1001-1-1"; +// +// // get the nice URL for 100111 +// var umbracoContextAccessor = GetUmbracoContextAccessor(url); +// var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); +// +// var urlProvider = new DefaultUrlProvider( +// Mock.Of>(x => x.CurrentValue == requestHandlerSettings), +// Mock.Of>(), +// new SiteDomainMapper(), +// umbracoContextAccessor, +// new UriUtility(Mock.Of()), +// Mock.Of()); +// var publishedUrlProvider = GetPublishedUrlProvider(umbracoContext, urlProvider); +// +// var absUrl = publishedUrlProvider.GetUrl(100111, UrlMode.Absolute); +// Assert.AreEqual("http://domain2.com/1001-1-1/", absUrl); +// +// const string cacheKeyPrefix = "NuCache.ContentCache.RouteByContent"; +// +// // check that the proper route has been cached +// var cache = (FastDictionaryAppCache)umbracoContext.PublishedSnapshot.ElementsCache; +// +// var cacheKey = $"{cacheKeyPrefix}[P:100111]"; +// Assert.AreEqual("10011/1001-1-1", cache.Get(cacheKey)); +// +// // route a rogue URL +// var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); +// var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); +// +// publishedRouter.FindAndSetDomain(frequest); +// Assert.IsTrue(frequest.HasDomain()); +// +// // check that it's been routed +// var lookup = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); +// var result = await lookup.TryFindContent(frequest); +// Assert.IsTrue(result); +// Assert.AreEqual(100111, frequest.PublishedContent.Id); +// +// // has the cache been polluted? +// Assert.AreEqual("10011/1001-1-1", cache.Get(cacheKey)); // no +// +// // what's the nice URL now? +// Assert.AreEqual("http://domain2.com/1001-1-1/", publishedUrlProvider.GetUrl(100111)); // good +// } +// +// private void SetDomains1() +// { +// var domainService = Mock.Get(DomainService); +// +// domainService.Setup(service => service.GetAll(It.IsAny())) +// .Returns((bool incWildcards) => new[] +// { +// new UmbracoDomain("http://domain1.com/") +// { +// Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US", +// }, +// new UmbracoDomain("http://domain2.com/") +// { +// Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", +// }, +// }); +// } +// +// private IPublishedUrlProvider GetPublishedUrlProvider(IUmbracoContext umbracoContext, DefaultUrlProvider urlProvider) +// { +// var webRoutingSettings = new WebRoutingSettings(); +// return new UrlProvider( +// new TestUmbracoContextAccessor(umbracoContext), +// Options.Create(webRoutingSettings), +// new UrlProviderCollection(() => new[] { urlProvider }), +// new MediaUrlProviderCollection(() => Enumerable.Empty()), +// Mock.Of()); +// } +// +// protected override string GetXmlContent(int templateId) +// => @" +// +// +// ]> +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// This is some content]]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// "; +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceTest.cs new file mode 100644 index 0000000000..52863f056f --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceTest.cs @@ -0,0 +1,100 @@ +using System.Data; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services; + +[TestFixture] +public class ContentNavigationServiceTest +{ + [Test] + public async Task Root_Is_1_Indexed() + { + var rootKey = Guid.NewGuid(); + IEnumerable navigationNodes = [new NavigationDto {Key = rootKey, ParentId = -1, Id = 1, Trashed = false}]; + var navigationRepoMock = new Mock(); + navigationRepoMock.Setup(x => x.GetContentNodesByObjectType(Constants.ObjectTypes.Document)) + .Returns(navigationNodes); + + var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object); + await contentNavigationService.RebuildAsync(); + + var success = contentNavigationService.TryGetLevel(rootKey, out var level); + + Assert.IsTrue(success); + Assert.That(level, Is.EqualTo(1)); + } + + [Test] + public async Task Can_Count_Child() + { + var rootKey = Guid.NewGuid(); + var childKey = Guid.NewGuid(); + var grandChildKey = Guid.NewGuid(); + IEnumerable navigationNodes = [ + new NavigationDto {Key = rootKey, ParentId = -1, Id = 1, Trashed = false}, + new NavigationDto {Key = childKey, ParentId = 1, Id = 2, Trashed = false}, + new NavigationDto {Key = grandChildKey, ParentId = 2, Id = 3, Trashed = false}]; + + var navigationRepoMock = new Mock(); + navigationRepoMock.Setup(x => x.GetContentNodesByObjectType(Constants.ObjectTypes.Document)) + .Returns(navigationNodes); + + var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object); + await contentNavigationService.RebuildAsync(); + + var success = contentNavigationService.TryGetLevel(childKey, out var level); + + Assert.IsTrue(success); + Assert.That(level, Is.EqualTo(2)); + } + + [Test] + public async Task Can_Count_Grandchild() + { + var rootKey = Guid.NewGuid(); + var childKey = Guid.NewGuid(); + var grandChildKey = Guid.NewGuid(); + IEnumerable navigationNodes = [ + new NavigationDto {Key = rootKey, ParentId = -1, Id = 1, Trashed = false}, + new NavigationDto {Key = childKey, ParentId = 1, Id = 2, Trashed = false}, + new NavigationDto {Key = grandChildKey, ParentId = 2, Id = 3, Trashed = false}]; + + var navigationRepoMock = new Mock(); + navigationRepoMock.Setup(x => x.GetContentNodesByObjectType(Constants.ObjectTypes.Document)) + .Returns(navigationNodes); + + var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object); + await contentNavigationService.RebuildAsync(); + + var success = contentNavigationService.TryGetLevel(grandChildKey, out var level); + + Assert.IsTrue(success); + Assert.That(level, Is.EqualTo(3)); + } + + public ICoreScopeProvider GetScopeProvider() + { + var mockScope = new Mock(); + var mockScopeProvider = new Mock(); + mockScopeProvider + .Setup(x => x.CreateCoreScope( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockScope.Object); + + return mockScopeProvider.Object; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs index 25aa4841ea..b98d1b1731 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs @@ -10,7 +10,9 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.UnitTests.TestHelpers.Objects; @@ -95,7 +97,10 @@ public class HtmlImageSourceParserTests Options.Create(webRoutingSettings), new UrlProviderCollection(() => Enumerable.Empty()), new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }), - Mock.Of()); + Mock.Of(), + Mock.Of(), + Mock.Of()); + using (var reference = umbracoContextFactory.EnsureUmbracoContext()) { var mediaCache = Mock.Get(reference.UmbracoContext.Media); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs index 67faeaf7ba..298cf5ddc4 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.UnitTests.TestHelpers.Objects; @@ -29,7 +30,7 @@ public class HtmlLocalLinkParserTests

media

"; - + var parser = new HtmlLocalLinkParser(Mock.Of()); var result = parser.FindUdisFromLocalLinks(input).ToList(); @@ -185,12 +186,11 @@ public class HtmlLocalLinkParserTests umbracoContextAccessor: umbracoContextAccessor); var webRoutingSettings = new WebRoutingSettings(); - var publishedUrlProvider = new UrlProvider( - umbracoContextAccessor, - Options.Create(webRoutingSettings), - new UrlProviderCollection(() => new[] { contentUrlProvider.Object }), - new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }), - Mock.Of()); + + var navigationQueryService = new Mock(); + Guid? parentKey = null; + navigationQueryService.Setup(x => x.TryGetParentKey(It.IsAny(), out parentKey)).Returns(true); + using (var reference = umbracoContextFactory.EnsureUmbracoContext()) { var contentCache = Mock.Get(reference.UmbracoContext.Content); @@ -201,6 +201,15 @@ public class HtmlLocalLinkParserTests mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); + var publishedUrlProvider = new UrlProvider( + umbracoContextAccessor, + Options.Create(webRoutingSettings), + new UrlProviderCollection(() => new[] { contentUrlProvider.Object }), + new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }), + Mock.Of(), + contentCache.Object, + navigationQueryService.Object); + var linkParser = new HtmlLocalLinkParser(publishedUrlProvider); var output = linkParser.EnsureInternalLinks(input); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs index 850aabbe04..04c6df0882 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs @@ -15,7 +15,6 @@ public class ApiRichTextMarkupParserTests { private Mock _apiContentRouteBuilder; private Mock _apiMediaUrlProvider; - private Mock _publishedSnapshotAccessor; [Test] public void Can_Parse_Legacy_LocalLinks() @@ -128,18 +127,6 @@ public class ApiRichTextMarkupParserTests mediaCacheMock.Setup(cc => cc.GetById(It.IsAny())) .Returns(udi => mockData[((GuidUdi)udi).Guid].PublishedContent); - var snapshotMock = new Mock(); - snapshotMock.SetupGet(ss => ss.Content) - .Returns(contentCacheMock.Object); - snapshotMock.SetupGet(ss => ss.Media) - .Returns(mediaCacheMock.Object); - - var snapShot = snapshotMock.Object; - - _publishedSnapshotAccessor = new Mock(); - _publishedSnapshotAccessor.Setup(psa => psa.TryGetPublishedSnapshot(out snapShot)) - .Returns(true); - _apiMediaUrlProvider = new Mock(); _apiMediaUrlProvider.Setup(mup => mup.GetUrl(It.IsAny())) .Returns(ipc => mockData[ipc.Key].MediaUrl); @@ -151,7 +138,8 @@ public class ApiRichTextMarkupParserTests return new ApiRichTextMarkupParser( _apiContentRouteBuilder.Object, _apiMediaUrlProvider.Object, - _publishedSnapshotAccessor.Object, + contentCacheMock.Object, + mediaCacheMock.Object, Mock.Of>()); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs index b70d036227..b17aeca984 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs @@ -77,7 +77,7 @@ public class MigrationPlanTests loggerFactory, migrationBuilder, databaseFactory, - Mock.Of(), distributedCache, Mock.Of(), Mock.Of()); + Mock.Of(), distributedCache, Mock.Of(), Mock.Of()); var plan = new MigrationPlan("default") .From(string.Empty) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/ContentSerializationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/ContentSerializationTests.cs index a6bc6439b7..8f885772db 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/ContentSerializationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/ContentSerializationTests.cs @@ -1,104 +1,105 @@ -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; - -[TestFixture] -public class ContentSerializationTests -{ - [Test] - public void GivenACacheModel_WhenItsSerializedAndDeserializedWithAnySerializer_TheResultsAreTheSame() - { - var jsonSerializer = new JsonContentNestedDataSerializer(); - var msgPackSerializer = new MsgPackContentNestedDataSerializer(Mock.Of()); - - var now = DateTime.Now; - var cacheModel = new ContentCacheDataModel - { - PropertyData = new Dictionary - { - ["propertyOne"] = - new[] { new PropertyData { Culture = "en-US", Segment = "test", Value = "hello world" } }, - ["propertyTwo"] = new[] - { - new PropertyData { Culture = "en-US", Segment = "test", Value = "Lorem ipsum" }, - }, - }, - CultureData = new Dictionary - { - ["en-US"] = new() { Date = now, IsDraft = false, Name = "Home", UrlSegment = "home" }, - }, - UrlSegment = "home", - }; - - var content = Mock.Of(x => x.ContentTypeId == 1); - - var json = jsonSerializer.Serialize(content, cacheModel, false).StringData; - var msgPack = msgPackSerializer.Serialize(content, cacheModel, false).ByteData; - - Console.WriteLine(json); - Console.WriteLine(msgPackSerializer.ToJson(msgPack)); - - var jsonContent = jsonSerializer.Deserialize(content, json, null, false); - var msgPackContent = msgPackSerializer.Deserialize(content, null, msgPack, false); - - CollectionAssert.AreEqual(jsonContent.CultureData.Keys, msgPackContent.CultureData.Keys); - CollectionAssert.AreEqual(jsonContent.PropertyData.Keys, msgPackContent.PropertyData.Keys); - CollectionAssert.AreEqual(jsonContent.CultureData.Values, msgPackContent.CultureData.Values, new CultureVariationComparer()); - CollectionAssert.AreEqual(jsonContent.PropertyData.Values, msgPackContent.PropertyData.Values, new PropertyDataComparer()); - Assert.AreEqual(jsonContent.UrlSegment, msgPackContent.UrlSegment); - } - - public class CultureVariationComparer : Comparer - { - public override int Compare(CultureVariation x, CultureVariation y) - { - if (x == null && y == null) - { - return 0; - } - - if (x == null && y != null) - { - return -1; - } - - if (x != null && y == null) - { - return 1; - } - - return x.Date.CompareTo(y.Date) | x.IsDraft.CompareTo(y.IsDraft) | x.Name.CompareTo(y.Name) | - x.UrlSegment.CompareTo(y.UrlSegment); - } - } - - public class PropertyDataComparer : Comparer - { - public override int Compare(PropertyData x, PropertyData y) - { - if (x == null && y == null) - { - return 0; - } - - if (x == null && y != null) - { - return -1; - } - - if (x != null && y == null) - { - return 1; - } - - var xVal = x.Value?.ToString() ?? string.Empty; - var yVal = y.Value?.ToString() ?? string.Empty; - - return x.Culture.CompareTo(y.Culture) | x.Segment.CompareTo(y.Segment) | xVal.CompareTo(yVal); - } - } -} +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.PropertyEditors; +// using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class ContentSerializationTests +// { +// [Test] +// public void GivenACacheModel_WhenItsSerializedAndDeserializedWithAnySerializer_TheResultsAreTheSame() +// { +// var jsonSerializer = new JsonContentNestedDataSerializer(); +// var msgPackSerializer = new MsgPackContentNestedDataSerializer(Mock.Of()); +// +// var now = DateTime.Now; +// var cacheModel = new ContentCacheDataModel +// { +// PropertyData = new Dictionary +// { +// ["propertyOne"] = +// new[] { new PropertyData { Culture = "en-US", Segment = "test", Value = "hello world" } }, +// ["propertyTwo"] = new[] +// { +// new PropertyData { Culture = "en-US", Segment = "test", Value = "Lorem ipsum" }, +// }, +// }, +// CultureData = new Dictionary +// { +// ["en-US"] = new() { Date = now, IsDraft = false, Name = "Home", UrlSegment = "home" }, +// }, +// UrlSegment = "home", +// }; +// +// var content = Mock.Of(x => x.ContentTypeId == 1); +// +// var json = jsonSerializer.Serialize(content, cacheModel, false).StringData; +// var msgPack = msgPackSerializer.Serialize(content, cacheModel, false).ByteData; +// +// Console.WriteLine(json); +// Console.WriteLine(msgPackSerializer.ToJson(msgPack)); +// +// var jsonContent = jsonSerializer.Deserialize(content, json, null, false); +// var msgPackContent = msgPackSerializer.Deserialize(content, null, msgPack, false); +// +// CollectionAssert.AreEqual(jsonContent.CultureData.Keys, msgPackContent.CultureData.Keys); +// CollectionAssert.AreEqual(jsonContent.PropertyData.Keys, msgPackContent.PropertyData.Keys); +// CollectionAssert.AreEqual(jsonContent.CultureData.Values, msgPackContent.CultureData.Values, new CultureVariationComparer()); +// CollectionAssert.AreEqual(jsonContent.PropertyData.Values, msgPackContent.PropertyData.Values, new PropertyDataComparer()); +// Assert.AreEqual(jsonContent.UrlSegment, msgPackContent.UrlSegment); +// } +// +// public class CultureVariationComparer : Comparer +// { +// public override int Compare(CultureVariation x, CultureVariation y) +// { +// if (x == null && y == null) +// { +// return 0; +// } +// +// if (x == null && y != null) +// { +// return -1; +// } +// +// if (x != null && y == null) +// { +// return 1; +// } +// +// return x.Date.CompareTo(y.Date) | x.IsDraft.CompareTo(y.IsDraft) | x.Name.CompareTo(y.Name) | +// x.UrlSegment.CompareTo(y.UrlSegment); +// } +// } +// +// public class PropertyDataComparer : Comparer +// { +// public override int Compare(PropertyData x, PropertyData y) +// { +// if (x == null && y == null) +// { +// return 0; +// } +// +// if (x == null && y != null) +// { +// return -1; +// } +// +// if (x != null && y == null) +// { +// return 1; +// } +// +// var xVal = x.Value?.ToString() ?? string.Empty; +// var yVal = y.Value?.ToString() ?? string.Empty; +// +// return x.Culture.CompareTo(y.Culture) | x.Segment.CompareTo(y.Segment) | xVal.CompareTo(yVal); +// } +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentCacheTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentCacheTests.cs index c06aed8415..e50550b033 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentCacheTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentCacheTests.cs @@ -1,74 +1,75 @@ -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; - -[TestFixture] -public class PublishContentCacheTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - var xml = PublishedContentXml.PublishContentCacheTestsXml(); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - // configure the Home content type to be composed of another for tests. - var compositionType = new ContentType(TestHelper.ShortStringHelper, -1) { Alias = "MyCompositionAlias" }; - contentTypes.First(x => x.Alias == "Home").AddContentType(compositionType); - - InitializedCache(kits, contentTypes, dataTypes); - - _cache = GetPublishedSnapshot().Content; - } - - private IPublishedContentCache _cache; - - [Test] - public void Has_Content() => Assert.IsTrue(_cache.HasContent()); - - [Test] - public void Get_Root_Docs() - { - var result = _cache.GetAtRoot().ToArray(); - Assert.AreEqual(2, result.Length); - Assert.AreEqual(1046, result.ElementAt(0).Id); - Assert.AreEqual(1172, result.ElementAt(1).Id); - } - - [TestCase("/", 1046)] - [TestCase("/home", 1046)] - [TestCase("/Home", 1046)] // test different cases - [TestCase("/home/sub1", 1173)] - [TestCase("/Home/sub1", 1173)] - [TestCase("/home/Sub1", 1173)] // test different cases - [TestCase("/home/Sub'Apostrophe", 1177)] - public void Get_Node_By_Route(string route, int nodeId) - { - var result = _cache.GetByRoute(route, false); - Assert.IsNotNull(result); - Assert.AreEqual(nodeId, result.Id); - } - - [TestCase("/", 1046)] - [TestCase("/sub1", 1173)] - [TestCase("/Sub1", 1173)] - public void Get_Node_By_Route_Hiding_Top_Level_Nodes(string route, int nodeId) - { - var result = _cache.GetByRoute(route, true); - Assert.IsNotNull(result); - Assert.AreEqual(nodeId, result.Id); - } -} +// using System.Collections.Generic; +// using System.Linq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Published; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class PublishContentCacheTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// var xml = PublishedContentXml.PublishContentCacheTestsXml(); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// // configure the Home content type to be composed of another for tests. +// var compositionType = new ContentType(TestHelper.ShortStringHelper, -1) { Alias = "MyCompositionAlias" }; +// contentTypes.First(x => x.Alias == "Home").AddContentType(compositionType); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// _cache = GetPublishedSnapshot().Content; +// } +// +// private IPublishedContentCache _cache; +// +// [Test] +// public void Has_Content() => Assert.IsTrue(_cache.HasContent()); +// +// [Test] +// public void Get_Root_Docs() +// { +// var result = _cache.GetAtRoot().ToArray(); +// Assert.AreEqual(2, result.Length); +// Assert.AreEqual(1046, result.ElementAt(0).Id); +// Assert.AreEqual(1172, result.ElementAt(1).Id); +// } +// +// [TestCase("/", 1046)] +// [TestCase("/home", 1046)] +// [TestCase("/Home", 1046)] // test different cases +// [TestCase("/home/sub1", 1173)] +// [TestCase("/Home/sub1", 1173)] +// [TestCase("/home/Sub1", 1173)] // test different cases +// [TestCase("/home/Sub'Apostrophe", 1177)] +// public void Get_Node_By_Route(string route, int nodeId) +// { +// var result = _cache.GetByRoute(route, false); +// Assert.IsNotNull(result); +// Assert.AreEqual(nodeId, result.Id); +// } +// +// [TestCase("/", 1046)] +// [TestCase("/sub1", 1173)] +// [TestCase("/Sub1", 1173)] +// public void Get_Node_By_Route_Hiding_Top_Level_Nodes(string route, int nodeId) +// { +// var result = _cache.GetByRoute(route, true); +// Assert.IsNotNull(result); +// Assert.AreEqual(nodeId, result.Id); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentDataTableTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentDataTableTests.cs index c5e1830381..0f10dc6027 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentDataTableTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentDataTableTests.cs @@ -1,195 +1,196 @@ -using System.Collections.Generic; -using System.Linq; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Builders.Extensions; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; - -/// -/// Unit tests for IPublishedContent and extensions -/// -[TestFixture] -public class PublishedContentDataTableTests : PublishedSnapshotServiceTestBase -{ - private readonly DataType[] _dataTypes = GetDefaultDataTypes(); - - private static ContentType CreateContentType(string name, IDataType dataType, IReadOnlyDictionary propertyAliasesAndNames) - { - var contentType = new ContentType(TestHelper.ShortStringHelper, -1) - { - Alias = name, - Name = name, - Key = Guid.NewGuid(), - Id = name.GetHashCode(), - }; - foreach (var prop in propertyAliasesAndNames) - { - contentType.AddPropertyType(new PropertyType(TestHelper.ShortStringHelper, dataType, prop.Key) - { - Name = prop.Value, - }); - } - - return contentType; - } - - private IEnumerable CreateCache( - bool createChildren, - IDataType dataType, - out ContentType[] contentTypes) - { - var result = new List(); - var valueCounter = 1; - var parentId = 3; - - var properties = new Dictionary { ["property1"] = "Property 1", ["property2"] = "Property 2" }; - - var parentContentType = CreateContentType( - "Parent", - dataType, - new Dictionary(properties) { ["property3"] = "Property 3" }); - var childContentType = CreateContentType( - "Child", - dataType, - new Dictionary(properties) { ["property4"] = "Property 4" }); - var child2ContentType = CreateContentType( - "Child2", - dataType, - new Dictionary(properties) { ["property4"] = "Property 4" }); - - contentTypes = new[] { parentContentType, childContentType, child2ContentType }; - - var parentData = new ContentDataBuilder() - .WithName("Page" + Guid.NewGuid()) - .WithProperties(new PropertyDataBuilder() - .WithPropertyData("property1", "value" + valueCounter) - .WithPropertyData("property2", "value" + (valueCounter + 1)) - .WithPropertyData("property3", "value" + (valueCounter + 2)) - .Build()) - .Build(); - - var parent = ContentNodeKitBuilder.CreateWithContent( - parentContentType.Id, - parentId, - $"-1,{parentId}", - draftData: parentData, - publishedData: parentData); - - result.Add(parent); - - if (createChildren) - { - for (var i = 0; i < 3; i++) - { - valueCounter += 3; - var childId = parentId + i + 1; - - var childData = new ContentDataBuilder() - .WithName("Page" + Guid.NewGuid()) - .WithProperties(new PropertyDataBuilder() - .WithPropertyData("property1", "value" + valueCounter) - .WithPropertyData("property2", "value" + (valueCounter + 1)) - .WithPropertyData("property4", "value" + (valueCounter + 2)) - .Build()) - .Build(); - - var child = ContentNodeKitBuilder.CreateWithContent( - i > 0 ? childContentType.Id : child2ContentType.Id, - childId, - $"-1,{parentId},{childId}", - i, - draftData: childData, - publishedData: childData); - - result.Add(child); - } - } - - return result; - } - - [Test] - public void To_DataTable() - { - var cache = CreateCache(true, _dataTypes[0], out var contentTypes); - InitializedCache(cache, contentTypes, _dataTypes); - - var snapshot = GetPublishedSnapshot(); - var root = snapshot.Content.GetAtRoot().First(); - - var dt = root.ChildrenAsTable( - VariationContextAccessor, - ContentTypeService, - MediaTypeService, - Mock.Of(), - Mock.Of()); - - Assert.AreEqual(11, dt.Columns.Count); - Assert.AreEqual(3, dt.Rows.Count); - Assert.AreEqual("value4", dt.Rows[0]["Property 1"]); - Assert.AreEqual("value5", dt.Rows[0]["Property 2"]); - Assert.AreEqual("value6", dt.Rows[0]["Property 4"]); - Assert.AreEqual("value7", dt.Rows[1]["Property 1"]); - Assert.AreEqual("value8", dt.Rows[1]["Property 2"]); - Assert.AreEqual("value9", dt.Rows[1]["Property 4"]); - Assert.AreEqual("value10", dt.Rows[2]["Property 1"]); - Assert.AreEqual("value11", dt.Rows[2]["Property 2"]); - Assert.AreEqual("value12", dt.Rows[2]["Property 4"]); - } - - [Test] - public void To_DataTable_With_Filter() - { - var cache = CreateCache(true, _dataTypes[0], out var contentTypes); - InitializedCache(cache, contentTypes, _dataTypes); - - var snapshot = GetPublishedSnapshot(); - var root = snapshot.Content.GetAtRoot().First(); - - var dt = root.ChildrenAsTable( - VariationContextAccessor, - ContentTypeService, - MediaTypeService, - Mock.Of(), - Mock.Of(), - "Child"); - - Assert.AreEqual(11, dt.Columns.Count); - Assert.AreEqual(2, dt.Rows.Count); - Assert.AreEqual("value7", dt.Rows[0]["Property 1"]); - Assert.AreEqual("value8", dt.Rows[0]["Property 2"]); - Assert.AreEqual("value9", dt.Rows[0]["Property 4"]); - Assert.AreEqual("value10", dt.Rows[1]["Property 1"]); - Assert.AreEqual("value11", dt.Rows[1]["Property 2"]); - Assert.AreEqual("value12", dt.Rows[1]["Property 4"]); - } - - [Test] - public void To_DataTable_No_Rows() - { - var cache = CreateCache(false, _dataTypes[0], out var contentTypes); - InitializedCache(cache, contentTypes, _dataTypes); - - var snapshot = GetPublishedSnapshot(); - var root = snapshot.Content.GetAtRoot().First(); - - var dt = root.ChildrenAsTable( - VariationContextAccessor, - ContentTypeService, - MediaTypeService, - Mock.Of(), - Mock.Of()); - - // will return an empty data table - Assert.AreEqual(0, dt.Columns.Count); - Assert.AreEqual(0, dt.Rows.Count); - } -} +// using System.Collections.Generic; +// using System.Linq; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Routing; +// using Umbraco.Cms.Core.Services; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Builders; +// using Umbraco.Cms.Tests.Common.Builders.Extensions; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; +// +// FIXME: Reintroduce if relevant +// /// +// /// Unit tests for IPublishedContent and extensions +// /// +// [TestFixture] +// public class PublishedContentDataTableTests : PublishedSnapshotServiceTestBase +// { +// private readonly DataType[] _dataTypes = GetDefaultDataTypes(); +// +// private static ContentType CreateContentType(string name, IDataType dataType, IReadOnlyDictionary propertyAliasesAndNames) +// { +// var contentType = new ContentType(TestHelper.ShortStringHelper, -1) +// { +// Alias = name, +// Name = name, +// Key = Guid.NewGuid(), +// Id = name.GetHashCode(), +// }; +// foreach (var prop in propertyAliasesAndNames) +// { +// contentType.AddPropertyType(new PropertyType(TestHelper.ShortStringHelper, dataType, prop.Key) +// { +// Name = prop.Value, +// }); +// } +// +// return contentType; +// } +// +// private IEnumerable CreateCache( +// bool createChildren, +// IDataType dataType, +// out ContentType[] contentTypes) +// { +// var result = new List(); +// var valueCounter = 1; +// var parentId = 3; +// +// var properties = new Dictionary { ["property1"] = "Property 1", ["property2"] = "Property 2" }; +// +// var parentContentType = CreateContentType( +// "Parent", +// dataType, +// new Dictionary(properties) { ["property3"] = "Property 3" }); +// var childContentType = CreateContentType( +// "Child", +// dataType, +// new Dictionary(properties) { ["property4"] = "Property 4" }); +// var child2ContentType = CreateContentType( +// "Child2", +// dataType, +// new Dictionary(properties) { ["property4"] = "Property 4" }); +// +// contentTypes = new[] { parentContentType, childContentType, child2ContentType }; +// +// var parentData = new ContentDataBuilder() +// .WithName("Page" + Guid.NewGuid()) +// .WithProperties(new PropertyDataBuilder() +// .WithPropertyData("property1", "value" + valueCounter) +// .WithPropertyData("property2", "value" + (valueCounter + 1)) +// .WithPropertyData("property3", "value" + (valueCounter + 2)) +// .Build()) +// .Build(); +// +// var parent = ContentNodeKitBuilder.CreateWithContent( +// parentContentType.Id, +// parentId, +// $"-1,{parentId}", +// draftData: parentData, +// publishedData: parentData); +// +// result.Add(parent); +// +// if (createChildren) +// { +// for (var i = 0; i < 3; i++) +// { +// valueCounter += 3; +// var childId = parentId + i + 1; +// +// var childData = new ContentDataBuilder() +// .WithName("Page" + Guid.NewGuid()) +// .WithProperties(new PropertyDataBuilder() +// .WithPropertyData("property1", "value" + valueCounter) +// .WithPropertyData("property2", "value" + (valueCounter + 1)) +// .WithPropertyData("property4", "value" + (valueCounter + 2)) +// .Build()) +// .Build(); +// +// var child = ContentNodeKitBuilder.CreateWithContent( +// i > 0 ? childContentType.Id : child2ContentType.Id, +// childId, +// $"-1,{parentId},{childId}", +// i, +// draftData: childData, +// publishedData: childData); +// +// result.Add(child); +// } +// } +// +// return result; +// } +// +// [Test] +// public void To_DataTable() +// { +// var cache = CreateCache(true, _dataTypes[0], out var contentTypes); +// InitializedCache(cache, contentTypes, _dataTypes); +// +// var snapshot = GetPublishedSnapshot(); +// var root = snapshot.Content.GetAtRoot().First(); +// +// var dt = root.ChildrenAsTable( +// VariationContextAccessor, +// ContentTypeService, +// MediaTypeService, +// Mock.Of(), +// Mock.Of()); +// +// Assert.AreEqual(11, dt.Columns.Count); +// Assert.AreEqual(3, dt.Rows.Count); +// Assert.AreEqual("value4", dt.Rows[0]["Property 1"]); +// Assert.AreEqual("value5", dt.Rows[0]["Property 2"]); +// Assert.AreEqual("value6", dt.Rows[0]["Property 4"]); +// Assert.AreEqual("value7", dt.Rows[1]["Property 1"]); +// Assert.AreEqual("value8", dt.Rows[1]["Property 2"]); +// Assert.AreEqual("value9", dt.Rows[1]["Property 4"]); +// Assert.AreEqual("value10", dt.Rows[2]["Property 1"]); +// Assert.AreEqual("value11", dt.Rows[2]["Property 2"]); +// Assert.AreEqual("value12", dt.Rows[2]["Property 4"]); +// } +// +// [Test] +// public void To_DataTable_With_Filter() +// { +// var cache = CreateCache(true, _dataTypes[0], out var contentTypes); +// InitializedCache(cache, contentTypes, _dataTypes); +// +// var snapshot = GetPublishedSnapshot(); +// var root = snapshot.Content.GetAtRoot().First(); +// +// var dt = root.ChildrenAsTable( +// VariationContextAccessor, +// ContentTypeService, +// MediaTypeService, +// Mock.Of(), +// Mock.Of(), +// "Child"); +// +// Assert.AreEqual(11, dt.Columns.Count); +// Assert.AreEqual(2, dt.Rows.Count); +// Assert.AreEqual("value7", dt.Rows[0]["Property 1"]); +// Assert.AreEqual("value8", dt.Rows[0]["Property 2"]); +// Assert.AreEqual("value9", dt.Rows[0]["Property 4"]); +// Assert.AreEqual("value10", dt.Rows[1]["Property 1"]); +// Assert.AreEqual("value11", dt.Rows[1]["Property 2"]); +// Assert.AreEqual("value12", dt.Rows[1]["Property 4"]); +// } +// +// [Test] +// public void To_DataTable_No_Rows() +// { +// var cache = CreateCache(false, _dataTypes[0], out var contentTypes); +// InitializedCache(cache, contentTypes, _dataTypes); +// +// var snapshot = GetPublishedSnapshot(); +// var root = snapshot.Content.GetAtRoot().First(); +// +// var dt = root.ChildrenAsTable( +// VariationContextAccessor, +// ContentTypeService, +// MediaTypeService, +// Mock.Of(), +// Mock.Of()); +// +// // will return an empty data table +// Assert.AreEqual(0, dt.Columns.Count); +// Assert.AreEqual(0, dt.Rows.Count); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentExtensionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentExtensionTests.cs index 0a71dd6faf..a86c0d4c22 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentExtensionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentExtensionTests.cs @@ -1,76 +1,77 @@ -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; - -[TestFixture] -public class PublishedContentExtensionTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - XmlContent, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - // configure inheritance for content types - var baseType = new ContentType(TestHelper.ShortStringHelper, -1) { Alias = "Base" }; - contentTypes[0].AddContentType(baseType); - - InitializedCache(kits, contentTypes, dataTypes); - } - - private const string XmlContent = @" - - -]> - - -"; - - [Test] - public void IsDocumentType_NonRecursive_ActualType_ReturnsTrue() - { - var publishedContent = GetContent(1100); - Assert.That(publishedContent.IsDocumentType("Inherited", false)); - } - - [Test] - public void IsDocumentType_NonRecursive_BaseType_ReturnsFalse() - { - var publishedContent = GetContent(1100); - Assert.That(publishedContent.IsDocumentType("Base", false), Is.False); - } - - [Test] - public void IsDocumentType_Recursive_ActualType_ReturnsTrue() - { - var publishedContent = GetContent(1100); - Assert.That(publishedContent.IsDocumentType("Inherited", true)); - } - - [Test] - public void IsDocumentType_Recursive_BaseType_ReturnsTrue() - { - var publishedContent = GetContent(1100); - Assert.That(publishedContent.IsDocumentType("Base", true)); - } - - [Test] - public void IsDocumentType_Recursive_InvalidBaseType_ReturnsFalse() - { - var publishedContent = GetContent(1100); - Assert.That(publishedContent.IsDocumentType("invalidbase", true), Is.False); - } -} +// using System.Collections.Generic; +// using System.Linq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Published; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class PublishedContentExtensionTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// XmlContent, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// // configure inheritance for content types +// var baseType = new ContentType(TestHelper.ShortStringHelper, -1) { Alias = "Base" }; +// contentTypes[0].AddContentType(baseType); +// +// InitializedCache(kits, contentTypes, dataTypes); +// } +// +// private const string XmlContent = @" +// +// +// ]> +// +// +// "; +// +// [Test] +// public void IsDocumentType_NonRecursive_ActualType_ReturnsTrue() +// { +// var publishedContent = GetContent(1100); +// Assert.That(publishedContent.IsDocumentType("Inherited", false)); +// } +// +// [Test] +// public void IsDocumentType_NonRecursive_BaseType_ReturnsFalse() +// { +// var publishedContent = GetContent(1100); +// Assert.That(publishedContent.IsDocumentType("Base", false), Is.False); +// } +// +// [Test] +// public void IsDocumentType_Recursive_ActualType_ReturnsTrue() +// { +// var publishedContent = GetContent(1100); +// Assert.That(publishedContent.IsDocumentType("Inherited", true)); +// } +// +// [Test] +// public void IsDocumentType_Recursive_BaseType_ReturnsTrue() +// { +// var publishedContent = GetContent(1100); +// Assert.That(publishedContent.IsDocumentType("Base", true)); +// } +// +// [Test] +// public void IsDocumentType_Recursive_InvalidBaseType_ReturnsFalse() +// { +// var publishedContent = GetContent(1100); +// Assert.That(publishedContent.IsDocumentType("invalidbase", true), Is.False); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentLanguageVariantTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentLanguageVariantTests.cs index 4f5ec06373..b6cb73b2a6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentLanguageVariantTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentLanguageVariantTests.cs @@ -1,376 +1,377 @@ -using System.Collections.Generic; -using System.Linq; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Builders.Extensions; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; - -[TestFixture] -public class PublishedContentLanguageVariantTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - var dataTypes = GetDefaultDataTypes(); - var cache = CreateCache(dataTypes, out var contentTypes); - - InitializedCache(cache, contentTypes, dataTypes); - } - - protected override PropertyValueConverterCollection PropertyValueConverterCollection - { - get - { - var collection = base.PropertyValueConverterCollection; - return new PropertyValueConverterCollection(() => collection.Append(new TestNoValueValueConverter())); - } - } - - private class TestNoValueValueConverter : SimpleTinyMceValueConverter - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.Alias == "noprop"; - - // for this test, we return false for IsValue for this property - public override bool? IsValue(object value, PropertyValueLevel level) => false; - } - - /// - /// Override to mock localization service - /// - /// - protected override ServiceContext CreateServiceContext(IContentType[] contentTypes, IMediaType[] mediaTypes, IDataType[] dataTypes) - { - var serviceContext = base.CreateServiceContext(contentTypes, mediaTypes, dataTypes); - - var localizationService = Mock.Get(serviceContext.LocalizationService); - - var languages = new List - { - new("en-US", "English (United States)") { Id = 1, IsDefault = true }, - new("fr", "French") { Id = 2 }, - new("es", "Spanish") { Id = 3, FallbackIsoCode = "en-US" }, - new("it", "Italian") { Id = 4, FallbackIsoCode = "es" }, - new("de", "German") { Id = 5 }, - new Language("da", "Danish") { Id = 6, FallbackIsoCode = "no" }, - new Language("sv", "Swedish") { Id = 7, FallbackIsoCode = "da" }, - new Language("no", "Norweigan") { Id = 8, FallbackIsoCode = "sv" }, - new Language("nl", "Dutch") { Id = 9, FallbackIsoCode = "en-US" }, - }; - - localizationService.Setup(x => x.GetAllLanguages()).Returns(languages); - localizationService.Setup(x => x.GetLanguageById(It.IsAny())) - .Returns((int id) => languages.SingleOrDefault(y => y.Id == id)); - localizationService.Setup(x => x.GetLanguageByIsoCode(It.IsAny())) - .Returns((string c) => languages.SingleOrDefault(y => y.IsoCode == c)); - - return serviceContext; - } - - /// - /// Creates a content cache - /// - /// - /// - /// - /// - /// Builds a content hierarchy of 3 nodes, each has a different set of cultural properties. - /// The first 2 share the same content type, the last one is a different content type. - /// NOTE: The content items themselves are 'Invariant' but their properties are 'Variant' by culture. - /// Normally in Umbraco this is prohibited but our APIs and database do actually support that behavior. - /// It is simpler to have these tests run this way, else we would need to use WithCultureInfos - /// for each item and pass in name values for all cultures we are supporting and then specify the - /// default VariationContextAccessor.VariationContext value to be a default culture instead of "". - /// - private IEnumerable CreateCache(IDataType[] dataTypes, out ContentType[] contentTypes) - { - var result = new List(); - - var propertyDataTypes = new Dictionary - { - // we only have one data type for this test which will be resolved with string empty. - [string.Empty] = dataTypes[0], - }; - - var contentType1 = new ContentType(ShortStringHelper, -1); - - var item1Data = new ContentDataBuilder() - .WithName("Content 1") - .WithProperties(new PropertyDataBuilder() - .WithPropertyData("welcomeText", "Welcome") - .WithPropertyData("welcomeText", "Welcome", "en-US") - .WithPropertyData("welcomeText", "Willkommen", "de") - .WithPropertyData("welcomeText", "Welkom", "nl") - .WithPropertyData("welcomeText2", "Welcome") - .WithPropertyData("welcomeText2", "Welcome", "en-US") - .WithPropertyData("noprop", "xxx") - .Build()) - - // build with a dynamically created content type - .Build(ShortStringHelper, propertyDataTypes, contentType1, "ContentType1"); - - var item1 = ContentNodeKitBuilder.CreateWithContent( - contentType1.Id, - 1, - "-1,1", - draftData: item1Data, - publishedData: item1Data); - - result.Add(item1); - - var item2Data = new ContentDataBuilder() - .WithName("Content 2") - .WithProperties(new PropertyDataBuilder() - .WithPropertyData("welcomeText", "Welcome") - .WithPropertyData("welcomeText", "Welcome", "en-US") - .WithPropertyData("numericField", 123) - .WithPropertyData("numericField", 123, "en-US") - .WithPropertyData("noprop", "xxx") - .Build()) - - // build while dynamically updating the same content type - .Build(ShortStringHelper, propertyDataTypes, contentType1); - - var item2 = ContentNodeKitBuilder.CreateWithContent( - contentType1.Id, - 2, - "-1,1,2", - parentContentId: 1, - draftData: item2Data, - publishedData: item2Data); - - result.Add(item2); - - var contentType2 = new ContentType(ShortStringHelper, -1); - - var item3Data = new ContentDataBuilder() - .WithName("Content 3") - .WithProperties(new PropertyDataBuilder() - .WithPropertyData("prop3", "Oxxo") - .WithPropertyData("prop3", "Oxxo", "en-US") - .Build()) - - // build with a dynamically created content type - .Build(ShortStringHelper, propertyDataTypes, contentType2, "ContentType2"); - - var item3 = ContentNodeKitBuilder.CreateWithContent( - contentType2.Id, - 3, - "-1,1,2,3", - parentContentId: 2, - draftData: item3Data, - publishedData: item3Data); - - result.Add(item3); - - contentTypes = new[] { contentType1, contentType2 }; - - return result; - } - - [Test] - public void Can_Get_Content_For_Populated_Requested_Language() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First(); - var value = content.Value(Mock.Of(), "welcomeText", "en-US"); - Assert.AreEqual("Welcome", value); - } - - [Test] - public void Can_Get_Content_For_Populated_Requested_Non_Default_Language() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First(); - var value = content.Value(Mock.Of(), "welcomeText", "de"); - Assert.AreEqual("Willkommen", value); - } - - [Test] - public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_Without_Fallback() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First(); - var value = content.Value(Mock.Of(), "welcomeText", "fr"); - Assert.IsNull(value); - } - - [Test] - public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Unless_Requested() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First(); - var value = content.Value(Mock.Of(), "welcomeText", "es"); - Assert.IsNull(value); - } - - [Test] - public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First(); - var value = content.Value(PublishedValueFallback, "welcomeText", "es", fallback: Fallback.ToLanguage); - Assert.AreEqual("Welcome", value); - } - - [Test] - public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First(); - var value = content.Value(PublishedValueFallback, "welcomeText", "it", fallback: Fallback.To(Fallback.Language, Fallback.Ancestors)); - Assert.AreEqual("Welcome", value); - } - - [Test] - public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First(); - var value = content.Value(Mock.Of(), "welcomeText", "no", fallback: Fallback.ToLanguage); - Assert.IsNull(value); - } - - [Test] - public void Can_Get_Content_For_Unpopulated_Requested_DefaultLanguage_With_Fallback() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First(); - var value = content.Value(PublishedValueFallback, "welcomeText", "fr", fallback: Fallback.ToDefaultLanguage); - Assert.AreEqual("Welcome", value); - } - - [Test] - public void Do_Not_Get_Content_Recursively_Unless_Requested() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First().Children.First(); - var value = content.Value(Mock.Of(), "welcomeText2"); - Assert.IsNull(value); - } - - [Test] - public void Can_Get_Content_Recursively() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First().Children.First(); - var value = content.Value(PublishedValueFallback, "welcomeText2", fallback: Fallback.ToAncestors); - Assert.AreEqual("Welcome", value); - } - - [Test] - public void Do_Not_Get_Content_Recursively_Unless_Requested2() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First().Children.First().Children.First(); - Assert.IsNull(content.GetProperty("welcomeText2")); - var value = content.Value(Mock.Of(), "welcomeText2"); - Assert.IsNull(value); - } - - [Test] - public void Can_Get_Content_Recursively2() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First().Children.First().Children.First(); - Assert.IsNull(content.GetProperty("welcomeText2")); - var value = content.Value(PublishedValueFallback, "welcomeText2", fallback: Fallback.ToAncestors); - Assert.AreEqual("Welcome", value); - } - - [Test] - public void Can_Get_Content_Recursively3() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First().Children.First().Children.First(); - Assert.IsNull(content.GetProperty("noprop")); - var value = content.Value(PublishedValueFallback, "noprop", fallback: Fallback.ToAncestors); - - // property has no value - based on the converter - // but we still get the value (ie, the converter would do something) - Assert.AreEqual("xxx", value.ToString()); - } - - [Test] - public void Can_Get_Content_With_Recursive_Priority() - { - VariationContextAccessor.VariationContext = new VariationContext("nl"); - - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First().Children.First(); - - var value = content.Value(PublishedValueFallback, "welcomeText", "nl", fallback: Fallback.To(Fallback.Ancestors, Fallback.Language)); - - // No Dutch value is directly assigned. Check has fallen back to Dutch value from parent. - Assert.AreEqual("Welkom", value); - } - - [Test] - public void Can_Get_Content_With_Fallback_Language_Priority() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First().Children.First(); - - var value = content.Value(PublishedValueFallback, "welcomeText", "nl", fallback: Fallback.ToLanguage); - var numericValue = content.Value(PublishedValueFallback, "numericField", "nl", fallback: Fallback.ToLanguage); - - // No Dutch value is directly assigned. Check has fallen back to English value from language variant. - Assert.AreEqual("Welcome", value); - Assert.AreEqual(123, numericValue); - } - - [Test] - public void Can_Get_Content_For_Property_With_Fallback_Language_Priority() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First().Children.First(); - - var value = content.GetProperty("welcomeText")!.Value(PublishedValueFallback, "nl", fallback: Fallback.ToLanguage); - var numericValue = content.GetProperty("numericField")!.Value(PublishedValueFallback, "nl", fallback: Fallback.ToLanguage); - - // No Dutch value is directly assigned. Check has fallen back to English value from language variant. - Assert.AreEqual("Welcome", value); - Assert.AreEqual(123, numericValue); - } - - [Test] - public void Throws_For_Non_Supported_Fallback() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First().Children.First(); - - Assert.Throws(() => - content.Value(PublishedValueFallback, "welcomeText", "nl", fallback: Fallback.To(999))); - } - - [Test] - public void Can_Fallback_To_Default_Value() - { - var snapshot = GetPublishedSnapshot(); - var content = snapshot.Content.GetAtRoot().First().Children.First(); - - // no Dutch value is assigned, so getting null - var value = content.Value(PublishedValueFallback, "welcomeText", "nl"); - Assert.IsNull(value); - - // even if we 'just' provide a default value - value = content.Value(PublishedValueFallback, "welcomeText", "nl", defaultValue: "woop"); - Assert.IsNull(value); - - // but it works with proper fallback settings - value = content.Value(PublishedValueFallback, "welcomeText", "nl", fallback: Fallback.ToDefaultValue, defaultValue: "woop"); - Assert.AreEqual("woop", value); - } -} +// using System.Collections.Generic; +// using System.Linq; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Core.PropertyEditors; +// using Umbraco.Cms.Core.PropertyEditors.ValueConverters; +// using Umbraco.Cms.Core.Services; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Builders; +// using Umbraco.Cms.Tests.Common.Builders.Extensions; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class PublishedContentLanguageVariantTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// var dataTypes = GetDefaultDataTypes(); +// var cache = CreateCache(dataTypes, out var contentTypes); +// +// InitializedCache(cache, contentTypes, dataTypes); +// } +// +// protected override PropertyValueConverterCollection PropertyValueConverterCollection +// { +// get +// { +// var collection = base.PropertyValueConverterCollection; +// return new PropertyValueConverterCollection(() => collection.Append(new TestNoValueValueConverter())); +// } +// } +// +// private class TestNoValueValueConverter : SimpleTinyMceValueConverter +// { +// public override bool IsConverter(IPublishedPropertyType propertyType) +// => propertyType.Alias == "noprop"; +// +// // for this test, we return false for IsValue for this property +// public override bool? IsValue(object value, PropertyValueLevel level) => false; +// } +// +// /// +// /// Override to mock localization service +// /// +// /// +// protected override ServiceContext CreateServiceContext(IContentType[] contentTypes, IMediaType[] mediaTypes, IDataType[] dataTypes) +// { +// var serviceContext = base.CreateServiceContext(contentTypes, mediaTypes, dataTypes); +// +// var localizationService = Mock.Get(serviceContext.LocalizationService); +// +// var languages = new List +// { +// new("en-US", "English (United States)") { Id = 1, IsDefault = true }, +// new("fr", "French") { Id = 2 }, +// new("es", "Spanish") { Id = 3, FallbackIsoCode = "en-US" }, +// new("it", "Italian") { Id = 4, FallbackIsoCode = "es" }, +// new("de", "German") { Id = 5 }, +// new Language("da", "Danish") { Id = 6, FallbackIsoCode = "no" }, +// new Language("sv", "Swedish") { Id = 7, FallbackIsoCode = "da" }, +// new Language("no", "Norweigan") { Id = 8, FallbackIsoCode = "sv" }, +// new Language("nl", "Dutch") { Id = 9, FallbackIsoCode = "en-US" }, +// }; +// +// localizationService.Setup(x => x.GetAllLanguages()).Returns(languages); +// localizationService.Setup(x => x.GetLanguageById(It.IsAny())) +// .Returns((int id) => languages.SingleOrDefault(y => y.Id == id)); +// localizationService.Setup(x => x.GetLanguageByIsoCode(It.IsAny())) +// .Returns((string c) => languages.SingleOrDefault(y => y.IsoCode == c)); +// +// return serviceContext; +// } +// +// /// +// /// Creates a content cache +// /// +// /// +// /// +// /// +// /// +// /// Builds a content hierarchy of 3 nodes, each has a different set of cultural properties. +// /// The first 2 share the same content type, the last one is a different content type. +// /// NOTE: The content items themselves are 'Invariant' but their properties are 'Variant' by culture. +// /// Normally in Umbraco this is prohibited but our APIs and database do actually support that behavior. +// /// It is simpler to have these tests run this way, else we would need to use WithCultureInfos +// /// for each item and pass in name values for all cultures we are supporting and then specify the +// /// default VariationContextAccessor.VariationContext value to be a default culture instead of "". +// /// +// private IEnumerable CreateCache(IDataType[] dataTypes, out ContentType[] contentTypes) +// { +// var result = new List(); +// +// var propertyDataTypes = new Dictionary +// { +// // we only have one data type for this test which will be resolved with string empty. +// [string.Empty] = dataTypes[0], +// }; +// +// var contentType1 = new ContentType(ShortStringHelper, -1); +// +// var item1Data = new ContentDataBuilder() +// .WithName("Content 1") +// .WithProperties(new PropertyDataBuilder() +// .WithPropertyData("welcomeText", "Welcome") +// .WithPropertyData("welcomeText", "Welcome", "en-US") +// .WithPropertyData("welcomeText", "Willkommen", "de") +// .WithPropertyData("welcomeText", "Welkom", "nl") +// .WithPropertyData("welcomeText2", "Welcome") +// .WithPropertyData("welcomeText2", "Welcome", "en-US") +// .WithPropertyData("noprop", "xxx") +// .Build()) +// +// // build with a dynamically created content type +// .Build(ShortStringHelper, propertyDataTypes, contentType1, "ContentType1"); +// +// var item1 = ContentNodeKitBuilder.CreateWithContent( +// contentType1.Id, +// 1, +// "-1,1", +// draftData: item1Data, +// publishedData: item1Data); +// +// result.Add(item1); +// +// var item2Data = new ContentDataBuilder() +// .WithName("Content 2") +// .WithProperties(new PropertyDataBuilder() +// .WithPropertyData("welcomeText", "Welcome") +// .WithPropertyData("welcomeText", "Welcome", "en-US") +// .WithPropertyData("numericField", 123) +// .WithPropertyData("numericField", 123, "en-US") +// .WithPropertyData("noprop", "xxx") +// .Build()) +// +// // build while dynamically updating the same content type +// .Build(ShortStringHelper, propertyDataTypes, contentType1); +// +// var item2 = ContentNodeKitBuilder.CreateWithContent( +// contentType1.Id, +// 2, +// "-1,1,2", +// parentContentId: 1, +// draftData: item2Data, +// publishedData: item2Data); +// +// result.Add(item2); +// +// var contentType2 = new ContentType(ShortStringHelper, -1); +// +// var item3Data = new ContentDataBuilder() +// .WithName("Content 3") +// .WithProperties(new PropertyDataBuilder() +// .WithPropertyData("prop3", "Oxxo") +// .WithPropertyData("prop3", "Oxxo", "en-US") +// .Build()) +// +// // build with a dynamically created content type +// .Build(ShortStringHelper, propertyDataTypes, contentType2, "ContentType2"); +// +// var item3 = ContentNodeKitBuilder.CreateWithContent( +// contentType2.Id, +// 3, +// "-1,1,2,3", +// parentContentId: 2, +// draftData: item3Data, +// publishedData: item3Data); +// +// result.Add(item3); +// +// contentTypes = new[] { contentType1, contentType2 }; +// +// return result; +// } +// +// [Test] +// public void Can_Get_Content_For_Populated_Requested_Language() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First(); +// var value = content.Value(Mock.Of(), "welcomeText", "en-US"); +// Assert.AreEqual("Welcome", value); +// } +// +// [Test] +// public void Can_Get_Content_For_Populated_Requested_Non_Default_Language() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First(); +// var value = content.Value(Mock.Of(), "welcomeText", "de"); +// Assert.AreEqual("Willkommen", value); +// } +// +// [Test] +// public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_Without_Fallback() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First(); +// var value = content.Value(Mock.Of(), "welcomeText", "fr"); +// Assert.IsNull(value); +// } +// +// [Test] +// public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Unless_Requested() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First(); +// var value = content.Value(Mock.Of(), "welcomeText", "es"); +// Assert.IsNull(value); +// } +// +// [Test] +// public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First(); +// var value = content.Value(PublishedValueFallback, "welcomeText", "es", fallback: Fallback.ToLanguage); +// Assert.AreEqual("Welcome", value); +// } +// +// [Test] +// public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First(); +// var value = content.Value(PublishedValueFallback, "welcomeText", "it", fallback: Fallback.To(Fallback.Language, Fallback.Ancestors)); +// Assert.AreEqual("Welcome", value); +// } +// +// [Test] +// public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First(); +// var value = content.Value(Mock.Of(), "welcomeText", "no", fallback: Fallback.ToLanguage); +// Assert.IsNull(value); +// } +// +// [Test] +// public void Can_Get_Content_For_Unpopulated_Requested_DefaultLanguage_With_Fallback() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First(); +// var value = content.Value(PublishedValueFallback, "welcomeText", "fr", fallback: Fallback.ToDefaultLanguage); +// Assert.AreEqual("Welcome", value); +// } +// +// [Test] +// public void Do_Not_Get_Content_Recursively_Unless_Requested() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First().Children.First(); +// var value = content.Value(Mock.Of(), "welcomeText2"); +// Assert.IsNull(value); +// } +// +// [Test] +// public void Can_Get_Content_Recursively() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First().Children.First(); +// var value = content.Value(PublishedValueFallback, "welcomeText2", fallback: Fallback.ToAncestors); +// Assert.AreEqual("Welcome", value); +// } +// +// [Test] +// public void Do_Not_Get_Content_Recursively_Unless_Requested2() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First().Children.First().Children.First(); +// Assert.IsNull(content.GetProperty("welcomeText2")); +// var value = content.Value(Mock.Of(), "welcomeText2"); +// Assert.IsNull(value); +// } +// +// [Test] +// public void Can_Get_Content_Recursively2() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First().Children.First().Children.First(); +// Assert.IsNull(content.GetProperty("welcomeText2")); +// var value = content.Value(PublishedValueFallback, "welcomeText2", fallback: Fallback.ToAncestors); +// Assert.AreEqual("Welcome", value); +// } +// +// [Test] +// public void Can_Get_Content_Recursively3() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First().Children.First().Children.First(); +// Assert.IsNull(content.GetProperty("noprop")); +// var value = content.Value(PublishedValueFallback, "noprop", fallback: Fallback.ToAncestors); +// +// // property has no value - based on the converter +// // but we still get the value (ie, the converter would do something) +// Assert.AreEqual("xxx", value.ToString()); +// } +// +// [Test] +// public void Can_Get_Content_With_Recursive_Priority() +// { +// VariationContextAccessor.VariationContext = new VariationContext("nl"); +// +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First().Children.First(); +// +// var value = content.Value(PublishedValueFallback, "welcomeText", "nl", fallback: Fallback.To(Fallback.Ancestors, Fallback.Language)); +// +// // No Dutch value is directly assigned. Check has fallen back to Dutch value from parent. +// Assert.AreEqual("Welkom", value); +// } +// +// [Test] +// public void Can_Get_Content_With_Fallback_Language_Priority() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First().Children.First(); +// +// var value = content.Value(PublishedValueFallback, "welcomeText", "nl", fallback: Fallback.ToLanguage); +// var numericValue = content.Value(PublishedValueFallback, "numericField", "nl", fallback: Fallback.ToLanguage); +// +// // No Dutch value is directly assigned. Check has fallen back to English value from language variant. +// Assert.AreEqual("Welcome", value); +// Assert.AreEqual(123, numericValue); +// } +// +// [Test] +// public void Can_Get_Content_For_Property_With_Fallback_Language_Priority() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First().Children.First(); +// +// var value = content.GetProperty("welcomeText")!.Value(PublishedValueFallback, "nl", fallback: Fallback.ToLanguage); +// var numericValue = content.GetProperty("numericField")!.Value(PublishedValueFallback, "nl", fallback: Fallback.ToLanguage); +// +// // No Dutch value is directly assigned. Check has fallen back to English value from language variant. +// Assert.AreEqual("Welcome", value); +// Assert.AreEqual(123, numericValue); +// } +// +// [Test] +// public void Throws_For_Non_Supported_Fallback() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First().Children.First(); +// +// Assert.Throws(() => +// content.Value(PublishedValueFallback, "welcomeText", "nl", fallback: Fallback.To(999))); +// } +// +// [Test] +// public void Can_Fallback_To_Default_Value() +// { +// var snapshot = GetPublishedSnapshot(); +// var content = snapshot.Content.GetAtRoot().First().Children.First(); +// +// // no Dutch value is assigned, so getting null +// var value = content.Value(PublishedValueFallback, "welcomeText", "nl"); +// Assert.IsNull(value); +// +// // even if we 'just' provide a default value +// value = content.Value(PublishedValueFallback, "welcomeText", "nl", defaultValue: "woop"); +// Assert.IsNull(value); +// +// // but it works with proper fallback settings +// value = content.Value(PublishedValueFallback, "welcomeText", "nl", fallback: Fallback.ToDefaultValue, defaultValue: "woop"); +// Assert.AreEqual("woop", value); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentTests.cs index 984085e754..15d55e6d21 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentTests.cs @@ -1,970 +1,971 @@ -using System.Collections.Generic; -using System.Linq; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; - -[TestFixture] -public class PublishedContentTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - var xml = PublishedContentXml.PublishedContentTestXml(1234, _node1173Guid); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - _dataTypes = dataTypes; - - // configure the Home content type to be composed of another for tests. - var compositionType = new ContentType(TestHelper.ShortStringHelper, -1) { Alias = "MyCompositionAlias" }; - contentTypes.First(x => x.Alias == "Home").AddContentType(compositionType); - - InitializedCache(kits, contentTypes, dataTypes); - } - - private readonly Guid _node1173Guid = Guid.NewGuid(); - private PublishedModelFactory _publishedModelFactory; - private DataType[] _dataTypes; - - // override to specify our own factory with custom types - protected override IPublishedModelFactory PublishedModelFactory - => _publishedModelFactory ??= new PublishedModelFactory( - new[] { typeof(Home), typeof(Anything), typeof(CustomDocument) }, - PublishedValueFallback); - - [PublishedModel("Home")] - internal class Home : PublishedContentModel - { - public Home(IPublishedContent content, IPublishedValueFallback fallback) - : base(content, fallback) - { - } - - public bool UmbracoNaviHide => this.Value(Mock.Of(), "umbracoNaviHide"); - } - - [PublishedModel("anything")] - internal class Anything : PublishedContentModel - { - public Anything(IPublishedContent content, IPublishedValueFallback fallback) - : base(content, fallback) - { - } - } - - [PublishedModel("CustomDocument")] - internal class CustomDocument : PublishedContentModel - { - public CustomDocument(IPublishedContent content, IPublishedValueFallback fallback) - : base(content, fallback) - { - } - } - - [Test] - public void GetNodeByIds() - { - var snapshot = GetPublishedSnapshot(); - - var contentById = snapshot.Content.GetById(1173); - Assert.IsNotNull(contentById); - var contentByGuid = snapshot.Content.GetById(_node1173Guid); - Assert.IsNotNull(contentByGuid); - Assert.AreEqual(contentById.Id, contentByGuid.Id); - Assert.AreEqual(contentById.Key, contentByGuid.Key); - - contentById = snapshot.Content.GetById(666); - Assert.IsNull(contentById); - contentByGuid = snapshot.Content.GetById(Guid.NewGuid()); - Assert.IsNull(contentByGuid); - } - - [Test] - public void Is_Last_From_Where_Filter_Dynamic_Linq() - { - var doc = GetContent(1173); - - var items = doc.Children(VariationContextAccessor).Where(x => x.IsVisible(Mock.Of())) - .ToIndexedArray(); - - foreach (var item in items) - { - if (item.Content.Id != 1178) - { - Assert.IsFalse(item.IsLast(), $"The item {item.Content.Id} is last"); - } - else - { - Assert.IsTrue(item.IsLast(), $"The item {item.Content.Id} is not last"); - } - } - } - - [Test] - public void Is_Last_From_Where_Filter() - { - var doc = GetContent(1173); - - var items = doc - .Children(VariationContextAccessor) - .Where(x => x.IsVisible(Mock.Of())) - .ToIndexedArray(); - - Assert.AreEqual(4, items.Length); - - foreach (var d in items) - { - switch (d.Content.Id) - { - case 1174: - Assert.IsTrue(d.IsFirst()); - Assert.IsFalse(d.IsLast()); - break; - case 117: - Assert.IsFalse(d.IsFirst()); - Assert.IsFalse(d.IsLast()); - break; - case 1177: - Assert.IsFalse(d.IsFirst()); - Assert.IsFalse(d.IsLast()); - break; - case 1178: - Assert.IsFalse(d.IsFirst()); - Assert.IsTrue(d.IsLast()); - break; - default: - Assert.Fail("Invalid id."); - break; - } - } - } - - [Test] - public void Is_Last_From_Where_Filter2() - { - var doc = GetContent(1173); - var ct = doc.ContentType; - - var items = doc.Children(VariationContextAccessor) - .Select(x => x.CreateModel(PublishedModelFactory)) // linq, returns IEnumerable - - // only way around this is to make sure every IEnumerable extension - // explicitely returns a PublishedContentSet, not an IEnumerable - .OfType() // ours, return IEnumerable (actually a PublishedContentSet) - .Where(x => x.IsVisible(Mock.Of())) // so, here it's linq again :-( - .ToIndexedArray(); // so, we need that one for the test to pass - - Assert.AreEqual(1, items.Length); - - foreach (var d in items) - { - switch (d.Content.Id) - { - case 1174: - Assert.IsTrue(d.IsFirst()); - Assert.IsTrue(d.IsLast()); - break; - default: - Assert.Fail("Invalid id."); - break; - } - } - } - - [Test] - public void Is_Last_From_Take() - { - var doc = GetContent(1173); - - var items = doc.Children(VariationContextAccessor).Take(4).ToIndexedArray(); - - foreach (var item in items) - { - if (item.Content.Id != 1178) - { - Assert.IsFalse(item.IsLast()); - } - else - { - Assert.IsTrue(item.IsLast()); - } - } - } - - [Test] - public void Is_Last_From_Skip() - { - var doc = GetContent(1173); - - foreach (var d in doc.Children(VariationContextAccessor).Skip(1).ToIndexedArray()) - { - if (d.Content.Id != 1176) - { - Assert.IsFalse(d.IsLast()); - } - else - { - Assert.IsTrue(d.IsLast()); - } - } - } - - [Test] - public void Is_Last_From_Concat() - { - var doc = GetContent(1173); - - var items = doc.Children(VariationContextAccessor) - .Concat(new[] { GetContent(1175), GetContent(4444) }) - .ToIndexedArray(); - - foreach (var item in items) - { - if (item.Content.Id != 4444) - { - Assert.IsFalse(item.IsLast()); - } - else - { - Assert.IsTrue(item.IsLast()); - } - } - } - - [Test] - public void Descendants_Ordered_Properly() - { - var doc = GetContent(1046); - - var expected = new[] { 1046, 1173, 1174, 117, 1177, 1178, 1179, 1176, 1175, 4444, 1172 }; - var exindex = 0; - - // must respect the XPath descendants-or-self axis! - foreach (var d in doc.DescendantsOrSelf(Mock.Of())) - { - Assert.AreEqual(expected[exindex++], d.Id); - } - } - - [Test] - public void Get_Property_Value_Recursive() - { - // TODO: We need to use a different fallback? - var doc = GetContent(1174); - var rVal = doc.Value(PublishedValueFallback, "testRecursive", fallback: Fallback.ToAncestors); - var nullVal = doc.Value(PublishedValueFallback, "DoNotFindThis", fallback: Fallback.ToAncestors); - Assert.AreEqual("This is the recursive val", rVal); - Assert.AreEqual(null, nullVal); - } - - [Test] - public void Get_Property_Value_Uses_Converter() - { - var doc = GetContent(1173); - - var propVal = doc.Value(PublishedValueFallback, "content"); - Assert.IsInstanceOf(typeof(IHtmlEncodedString), propVal); - Assert.AreEqual("
This is some content
", propVal.ToString()); - - var propVal2 = doc.Value(PublishedValueFallback, "content"); - Assert.IsInstanceOf(typeof(IHtmlEncodedString), propVal2); - Assert.AreEqual("
This is some content
", propVal2.ToString()); - - var propVal3 = doc.Value(PublishedValueFallback, "Content"); - Assert.IsInstanceOf(typeof(IHtmlEncodedString), propVal3); - Assert.AreEqual("
This is some content
", propVal3.ToString()); - } - - [Test] - public void Complex_Linq() - { - var doc = GetContent(1173); - - var result = doc.Ancestors().OrderBy(x => x.Level) - .Single() - .Descendants(Mock.Of()) - .FirstOrDefault(x => - x.Value(PublishedValueFallback, "selectedNodes", fallback: Fallback.ToDefaultValue, defaultValue: string.Empty).Split(',').Contains("1173")); - - Assert.IsNotNull(result); - } - - [Test] - public void Children_GroupBy_DocumentTypeAlias() - { - // var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { }); - // var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { }); - // var contentTypes = new Dictionary - // { - // { home.Alias, home }, - // { custom.Alias, custom } - // }; - // ContentTypesCache.GetPublishedContentTypeByAlias = alias => contentTypes[alias]; - var doc = GetContent(1046); - - var found1 = doc.Children(VariationContextAccessor).GroupBy(x => x.ContentType.Alias).ToArray(); - - Assert.AreEqual(2, found1.Length); - Assert.AreEqual(2, found1.Single(x => x.Key.ToString() == "Home").Count()); - Assert.AreEqual(1, found1.Single(x => x.Key.ToString() == "CustomDocument").Count()); - } - - [Test] - public void Children_Where_DocumentTypeAlias() - { - // var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { }); - // var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { }); - // var contentTypes = new Dictionary - // { - // { home.Alias, home }, - // { custom.Alias, custom } - // }; - // ContentTypesCache.GetPublishedContentTypeByAlias = alias => contentTypes[alias]; - var doc = GetContent(1046); - - var found1 = doc.Children(VariationContextAccessor).Where(x => x.ContentType.Alias == "CustomDocument"); - var found2 = doc.Children(VariationContextAccessor).Where(x => x.ContentType.Alias == "Home"); - - Assert.AreEqual(1, found1.Count()); - Assert.AreEqual(2, found2.Count()); - } - - [Test] - public void Children_Order_By_Update_Date() - { - var doc = GetContent(1173); - - var ordered = doc.Children(VariationContextAccessor).OrderBy(x => x.UpdateDate); - - var correctOrder = new[] { 1178, 1177, 1174, 1176 }; - for (var i = 0; i < correctOrder.Length; i++) - { - Assert.AreEqual(correctOrder[i], ordered.ElementAt(i).Id); - } - } - - [Test] - public void FirstChild() - { - var doc = GetContent(1173); // has child nodes - Assert.IsNotNull(doc.FirstChild(Mock.Of())); - Assert.IsNotNull(doc.FirstChild(Mock.Of(), x => true)); - Assert.IsNotNull(doc.FirstChild(Mock.Of())); - - doc = GetContent(1175); // does not have child nodes - Assert.IsNull(doc.FirstChild(Mock.Of())); - Assert.IsNull(doc.FirstChild(Mock.Of(), x => true)); - Assert.IsNull(doc.FirstChild(Mock.Of())); - } - - [Test] - public void FirstChildAsT() - { - var doc = GetContent(1046); // has child nodes - - var model = doc.FirstChild(Mock.Of(), x => true); // predicate - - Assert.IsNotNull(model); - Assert.IsTrue(model.Id == 1173); - Assert.IsInstanceOf(model); - Assert.IsInstanceOf(model); - - doc = GetContent(1175); // does not have child nodes - Assert.IsNull(doc.FirstChild(Mock.Of())); - Assert.IsNull(doc.FirstChild(Mock.Of(), x => true)); - } - - [Test] - public void IsComposedOf() - { - var doc = GetContent(1173); - - var isComposedOf = doc.IsComposedOf("MyCompositionAlias"); - - Assert.IsTrue(isComposedOf); - } - - [Test] - public void HasProperty() - { - var doc = GetContent(1173); - - var hasProp = doc.HasProperty(Constants.Conventions.Content.UrlAlias); - - Assert.IsTrue(hasProp); - } - - [Test] - public void HasValue() - { - var doc = GetContent(1173); - - var hasValue = doc.HasValue(Mock.Of(), Constants.Conventions.Content.UrlAlias); - var noValue = doc.HasValue(Mock.Of(), "blahblahblah"); - - Assert.IsTrue(hasValue); - Assert.IsFalse(noValue); - } - - [Test] - public void Ancestors_Where_Visible() - { - var doc = GetContent(1174); - - var whereVisible = doc.Ancestors().Where(x => x.IsVisible(Mock.Of())); - Assert.AreEqual(1, whereVisible.Count()); - } - - [Test] - public void Visible() - { - var hidden = GetContent(1046); - var visible = GetContent(1173); - - Assert.IsFalse(hidden.IsVisible(Mock.Of())); - Assert.IsTrue(visible.IsVisible(Mock.Of())); - } - - [Test] - public void Ancestor_Or_Self() - { - var doc = GetContent(1173); - - var result = doc.AncestorOrSelf(); - - Assert.IsNotNull(result); - - // ancestor-or-self has to be self! - Assert.AreEqual(1173, result.Id); - } - - [Test] - public void U4_4559() - { - var doc = GetContent(1174); - var result = doc.AncestorOrSelf(1); - Assert.IsNotNull(result); - Assert.AreEqual(1046, result.Id); - } - - [Test] - public void Ancestors_Or_Self() - { - var doc = GetContent(1174); - - var result = doc.AncestorsOrSelf().ToArray(); - - Assert.IsNotNull(result); - - Assert.AreEqual(3, result.Length); - Assert.IsTrue(result.Select(x => x.Id).ContainsAll(new[] { 1174, 1173, 1046 })); - } - - [Test] - public void Ancestors() - { - var doc = GetContent(1174); - - var result = doc.Ancestors().ToArray(); - - Assert.IsNotNull(result); - - Assert.AreEqual(2, result.Length); - Assert.IsTrue(result.Select(x => x.Id).ContainsAll(new[] { 1173, 1046 })); - } - - [Test] - public void IsAncestor() - { - // Structure: - // - Root : 1046 (no parent) - // -- Home: 1173 (parent 1046) - // -- Custom Doc: 1178 (parent 1173) - // --- Custom Doc2: 1179 (parent: 1178) - // -- Custom Doc4: 117 (parent 1173) - // - Custom Doc3: 1172 (no parent) - var home = GetContent(1173); - var root = GetContent(1046); - var customDoc = GetContent(1178); - var customDoc2 = GetContent(1179); - var customDoc3 = GetContent(1172); - var customDoc4 = GetContent(117); - - Assert.IsTrue(root.IsAncestor(customDoc4)); - Assert.IsFalse(root.IsAncestor(customDoc3)); - Assert.IsTrue(root.IsAncestor(customDoc2)); - Assert.IsTrue(root.IsAncestor(customDoc)); - Assert.IsTrue(root.IsAncestor(home)); - Assert.IsFalse(root.IsAncestor(root)); - - Assert.IsTrue(home.IsAncestor(customDoc4)); - Assert.IsFalse(home.IsAncestor(customDoc3)); - Assert.IsTrue(home.IsAncestor(customDoc2)); - Assert.IsTrue(home.IsAncestor(customDoc)); - Assert.IsFalse(home.IsAncestor(home)); - Assert.IsFalse(home.IsAncestor(root)); - - Assert.IsFalse(customDoc.IsAncestor(customDoc4)); - Assert.IsFalse(customDoc.IsAncestor(customDoc3)); - Assert.IsTrue(customDoc.IsAncestor(customDoc2)); - Assert.IsFalse(customDoc.IsAncestor(customDoc)); - Assert.IsFalse(customDoc.IsAncestor(home)); - Assert.IsFalse(customDoc.IsAncestor(root)); - - Assert.IsFalse(customDoc2.IsAncestor(customDoc4)); - Assert.IsFalse(customDoc2.IsAncestor(customDoc3)); - Assert.IsFalse(customDoc2.IsAncestor(customDoc2)); - Assert.IsFalse(customDoc2.IsAncestor(customDoc)); - Assert.IsFalse(customDoc2.IsAncestor(home)); - Assert.IsFalse(customDoc2.IsAncestor(root)); - - Assert.IsFalse(customDoc3.IsAncestor(customDoc3)); - } - - [Test] - public void IsAncestorOrSelf() - { - // Structure: - // - Root : 1046 (no parent) - // -- Home: 1173 (parent 1046) - // -- Custom Doc: 1178 (parent 1173) - // --- Custom Doc2: 1179 (parent: 1178) - // -- Custom Doc4: 117 (parent 1173) - // - Custom Doc3: 1172 (no parent) - var home = GetContent(1173); - var root = GetContent(1046); - var customDoc = GetContent(1178); - var customDoc2 = GetContent(1179); - var customDoc3 = GetContent(1172); - var customDoc4 = GetContent(117); - - Assert.IsTrue(root.IsAncestorOrSelf(customDoc4)); - Assert.IsFalse(root.IsAncestorOrSelf(customDoc3)); - Assert.IsTrue(root.IsAncestorOrSelf(customDoc2)); - Assert.IsTrue(root.IsAncestorOrSelf(customDoc)); - Assert.IsTrue(root.IsAncestorOrSelf(home)); - Assert.IsTrue(root.IsAncestorOrSelf(root)); - - Assert.IsTrue(home.IsAncestorOrSelf(customDoc4)); - Assert.IsFalse(home.IsAncestorOrSelf(customDoc3)); - Assert.IsTrue(home.IsAncestorOrSelf(customDoc2)); - Assert.IsTrue(home.IsAncestorOrSelf(customDoc)); - Assert.IsTrue(home.IsAncestorOrSelf(home)); - Assert.IsFalse(home.IsAncestorOrSelf(root)); - - Assert.IsFalse(customDoc.IsAncestorOrSelf(customDoc4)); - Assert.IsFalse(customDoc.IsAncestorOrSelf(customDoc3)); - Assert.IsTrue(customDoc.IsAncestorOrSelf(customDoc2)); - Assert.IsTrue(customDoc.IsAncestorOrSelf(customDoc)); - Assert.IsFalse(customDoc.IsAncestorOrSelf(home)); - Assert.IsFalse(customDoc.IsAncestorOrSelf(root)); - - Assert.IsFalse(customDoc2.IsAncestorOrSelf(customDoc4)); - Assert.IsFalse(customDoc2.IsAncestorOrSelf(customDoc3)); - Assert.IsTrue(customDoc2.IsAncestorOrSelf(customDoc2)); - Assert.IsFalse(customDoc2.IsAncestorOrSelf(customDoc)); - Assert.IsFalse(customDoc2.IsAncestorOrSelf(home)); - Assert.IsFalse(customDoc2.IsAncestorOrSelf(root)); - - Assert.IsTrue(customDoc4.IsAncestorOrSelf(customDoc4)); - Assert.IsTrue(customDoc3.IsAncestorOrSelf(customDoc3)); - } - - [Test] - public void Descendants_Or_Self() - { - var doc = GetContent(1046); - - var result = doc.DescendantsOrSelf(Mock.Of()).ToArray(); - - Assert.IsNotNull(result); - - Assert.AreEqual(10, result.Count()); - Assert.IsTrue(result.Select(x => x.Id).ContainsAll(new[] { 1046, 1173, 1174, 1176, 1175 })); - } - - [Test] - public void Descendants() - { - var doc = GetContent(1046); - - var result = doc.Descendants(Mock.Of()).ToArray(); - - Assert.IsNotNull(result); - - Assert.AreEqual(9, result.Count()); - Assert.IsTrue(result.Select(x => x.Id).ContainsAll(new[] { 1173, 1174, 1176, 1175, 4444 })); - } - - [Test] - public void IsDescendant() - { - // Structure: - // - Root : 1046 (no parent) - // -- Home: 1173 (parent 1046) - // -- Custom Doc: 1178 (parent 1173) - // --- Custom Doc2: 1179 (parent: 1178) - // -- Custom Doc4: 117 (parent 1173) - // - Custom Doc3: 1172 (no parent) - var home = GetContent(1173); - var root = GetContent(1046); - var customDoc = GetContent(1178); - var customDoc2 = GetContent(1179); - var customDoc3 = GetContent(1172); - var customDoc4 = GetContent(117); - - Assert.IsFalse(root.IsDescendant(root)); - Assert.IsFalse(root.IsDescendant(home)); - Assert.IsFalse(root.IsDescendant(customDoc)); - Assert.IsFalse(root.IsDescendant(customDoc2)); - Assert.IsFalse(root.IsDescendant(customDoc3)); - Assert.IsFalse(root.IsDescendant(customDoc4)); - - Assert.IsTrue(home.IsDescendant(root)); - Assert.IsFalse(home.IsDescendant(home)); - Assert.IsFalse(home.IsDescendant(customDoc)); - Assert.IsFalse(home.IsDescendant(customDoc2)); - Assert.IsFalse(home.IsDescendant(customDoc3)); - Assert.IsFalse(home.IsDescendant(customDoc4)); - - Assert.IsTrue(customDoc.IsDescendant(root)); - Assert.IsTrue(customDoc.IsDescendant(home)); - Assert.IsFalse(customDoc.IsDescendant(customDoc)); - Assert.IsFalse(customDoc.IsDescendant(customDoc2)); - Assert.IsFalse(customDoc.IsDescendant(customDoc3)); - Assert.IsFalse(customDoc.IsDescendant(customDoc4)); - - Assert.IsTrue(customDoc2.IsDescendant(root)); - Assert.IsTrue(customDoc2.IsDescendant(home)); - Assert.IsTrue(customDoc2.IsDescendant(customDoc)); - Assert.IsFalse(customDoc2.IsDescendant(customDoc2)); - Assert.IsFalse(customDoc2.IsDescendant(customDoc3)); - Assert.IsFalse(customDoc2.IsDescendant(customDoc4)); - - Assert.IsFalse(customDoc3.IsDescendant(customDoc3)); - } - - [Test] - public void IsDescendantOrSelf() - { - // Structure: - // - Root : 1046 (no parent) - // -- Home: 1173 (parent 1046) - // -- Custom Doc: 1178 (parent 1173) - // --- Custom Doc2: 1179 (parent: 1178) - // -- Custom Doc4: 117 (parent 1173) - // - Custom Doc3: 1172 (no parent) - var home = GetContent(1173); - var root = GetContent(1046); - var customDoc = GetContent(1178); - var customDoc2 = GetContent(1179); - var customDoc3 = GetContent(1172); - var customDoc4 = GetContent(117); - - Assert.IsTrue(root.IsDescendantOrSelf(root)); - Assert.IsFalse(root.IsDescendantOrSelf(home)); - Assert.IsFalse(root.IsDescendantOrSelf(customDoc)); - Assert.IsFalse(root.IsDescendantOrSelf(customDoc2)); - Assert.IsFalse(root.IsDescendantOrSelf(customDoc3)); - Assert.IsFalse(root.IsDescendantOrSelf(customDoc4)); - - Assert.IsTrue(home.IsDescendantOrSelf(root)); - Assert.IsTrue(home.IsDescendantOrSelf(home)); - Assert.IsFalse(home.IsDescendantOrSelf(customDoc)); - Assert.IsFalse(home.IsDescendantOrSelf(customDoc2)); - Assert.IsFalse(home.IsDescendantOrSelf(customDoc3)); - Assert.IsFalse(home.IsDescendantOrSelf(customDoc4)); - - Assert.IsTrue(customDoc.IsDescendantOrSelf(root)); - Assert.IsTrue(customDoc.IsDescendantOrSelf(home)); - Assert.IsTrue(customDoc.IsDescendantOrSelf(customDoc)); - Assert.IsFalse(customDoc.IsDescendantOrSelf(customDoc2)); - Assert.IsFalse(customDoc.IsDescendantOrSelf(customDoc3)); - Assert.IsFalse(customDoc.IsDescendantOrSelf(customDoc4)); - - Assert.IsTrue(customDoc2.IsDescendantOrSelf(root)); - Assert.IsTrue(customDoc2.IsDescendantOrSelf(home)); - Assert.IsTrue(customDoc2.IsDescendantOrSelf(customDoc)); - Assert.IsTrue(customDoc2.IsDescendantOrSelf(customDoc2)); - Assert.IsFalse(customDoc2.IsDescendantOrSelf(customDoc3)); - Assert.IsFalse(customDoc2.IsDescendantOrSelf(customDoc4)); - - Assert.IsTrue(customDoc3.IsDescendantOrSelf(customDoc3)); - } - - [Test] - public void SiblingsAndSelf() - { - // Structure: - // - Root : 1046 (no parent) - // -- Level1.1: 1173 (parent 1046) - // --- Level1.1.1: 1174 (parent 1173) - // --- Level1.1.2: 117 (parent 1173) - // --- Level1.1.3: 1177 (parent 1173) - // --- Level1.1.4: 1178 (parent 1173) - // ---- Level1.1.4.1: 1179 (parent 1178) - // --- Level1.1.5: 1176 (parent 1173) - // -- Level1.2: 1175 (parent 1046) - // -- Level1.3: 4444 (parent 1046) - // - Root : 1172 (no parent) - var root = GetContent(1046); - var level1_1 = GetContent(1173); - var level1_1_1 = GetContent(1174); - var level1_1_2 = GetContent(117); - var level1_1_3 = GetContent(1177); - var level1_1_4 = GetContent(1178); - var level1_1_5 = GetContent(1176); - var level1_2 = GetContent(1175); - var level1_3 = GetContent(4444); - var root2 = GetContent(1172); - - var publishedSnapshot = GetPublishedSnapshot(); - - CollectionAssertAreEqual(new[] { root, root2 }, root.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); - - CollectionAssertAreEqual(new[] { level1_1, level1_2, level1_3 }, level1_1.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); - CollectionAssertAreEqual(new[] { level1_1, level1_2, level1_3 }, level1_2.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); - CollectionAssertAreEqual(new[] { level1_1, level1_2, level1_3 }, level1_3.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); - - CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_1.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); - CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_2.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); - CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_3.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); - CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_4.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); - CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_5.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); - } - - [Test] - public void Siblings() - { - // Structure: - // - Root : 1046 (no parent) - // -- Level1.1: 1173 (parent 1046) - // --- Level1.1.1: 1174 (parent 1173) - // --- Level1.1.2: 117 (parent 1173) - // --- Level1.1.3: 1177 (parent 1173) - // --- Level1.1.4: 1178 (parent 1173) - // ---- Level1.1.4.1: 1179 (parent 1178) - // --- Level1.1.5: 1176 (parent 1173) - // -- Level1.2: 1175 (parent 1046) - // -- Level1.3: 4444 (parent 1046) - // - Root : 1172 (no parent) - var root = GetContent(1046); - var level1_1 = GetContent(1173); - var level1_1_1 = GetContent(1174); - var level1_1_2 = GetContent(117); - var level1_1_3 = GetContent(1177); - var level1_1_4 = GetContent(1178); - var level1_1_5 = GetContent(1176); - var level1_2 = GetContent(1175); - var level1_3 = GetContent(4444); - var root2 = GetContent(1172); - - var publishedSnapshot = GetPublishedSnapshot(); - - CollectionAssertAreEqual(new[] { root2 }, root.Siblings(publishedSnapshot, VariationContextAccessor)); - - CollectionAssertAreEqual(new[] { level1_2, level1_3 }, level1_1.Siblings(publishedSnapshot, VariationContextAccessor)); - CollectionAssertAreEqual(new[] { level1_1, level1_3 }, level1_2.Siblings(publishedSnapshot, VariationContextAccessor)); - CollectionAssertAreEqual(new[] { level1_1, level1_2 }, level1_3.Siblings(publishedSnapshot, VariationContextAccessor)); - - CollectionAssertAreEqual(new[] { level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_1.Siblings(publishedSnapshot, VariationContextAccessor)); - CollectionAssertAreEqual(new[] { level1_1_1, level1_1_3, level1_1_4, level1_1_5 }, level1_1_2.Siblings(publishedSnapshot, VariationContextAccessor)); - CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_4, level1_1_5 }, level1_1_3.Siblings(publishedSnapshot, VariationContextAccessor)); - CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_5 }, level1_1_4.Siblings(publishedSnapshot, VariationContextAccessor)); - CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4 }, level1_1_5.Siblings(publishedSnapshot, VariationContextAccessor)); - } - - private void CollectionAssertAreEqual(IEnumerable expected, IEnumerable actual) - where T : IPublishedContent - { - var e = expected.Select(x => x.Id).ToArray(); - var a = actual.Select(x => x.Id).ToArray(); - CollectionAssert.AreEquivalent(e, a, $"\nExpected:\n{string.Join(", ", e)}\n\nActual:\n{string.Join(", ", a)}"); - } - - [Test] - public void FragmentProperty() - { - IEnumerable CreatePropertyTypes(IPublishedContentType contentType) - { - yield return PublishedContentTypeFactory.CreatePropertyType(contentType, "detached", _dataTypes[0].Id); - } - - var ct = PublishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes); - var pt = ct.GetPropertyType("detached"); - var prop = new PublishedElementPropertyBase(pt, null, false, PropertyCacheLevel.None, 5548); - Assert.IsInstanceOf(prop.GetValue()); - Assert.AreEqual(5548, prop.GetValue()); - } - - [Test] - public void Fragment2() - { - IEnumerable CreatePropertyTypes(IPublishedContentType contentType) - { - yield return PublishedContentTypeFactory.CreatePropertyType(contentType, "legend", _dataTypes[0].Id); - yield return PublishedContentTypeFactory.CreatePropertyType(contentType, "image", _dataTypes[0].Id); - yield return PublishedContentTypeFactory.CreatePropertyType(contentType, "size", _dataTypes[0].Id); - } - - const string val1 = "boom bam"; - const int val2 = 0; - const int val3 = 666; - - var guid = Guid.NewGuid(); - - var ct = PublishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes); - - var c = new ImageWithLegendModel( - ct, - guid, - new Dictionary { { "legend", val1 }, { "image", val2 }, { "size", val3 } }, - false); - - Assert.AreEqual(val1, c.Legend); - Assert.AreEqual(val3, c.Size); - } - - [Test] - public void First() - { - var publishedSnapshot = GetPublishedSnapshot(); - var content = publishedSnapshot.Content.GetAtRoot().First(); - Assert.AreEqual("Home", content.Name(VariationContextAccessor)); - } - - [Test] - public void Distinct() - { - var items = GetContent(1173) - .Children(VariationContextAccessor) - .Distinct() - .Distinct() - .ToIndexedArray(); - - Assert.AreEqual(5, items.Length); - - var item = items[0]; - Assert.AreEqual(1174, item.Content.Id); - Assert.IsTrue(item.IsFirst()); - Assert.IsFalse(item.IsLast()); - - item = items[^1]; - Assert.AreEqual(1176, item.Content.Id); - Assert.IsFalse(item.IsFirst()); - Assert.IsTrue(item.IsLast()); - } - - [Test] - public void OfType1() - { - var publishedSnapshot = GetPublishedSnapshot(); - var items = publishedSnapshot.Content.GetAtRoot() - .OfType() - .Distinct() - .ToIndexedArray(); - Assert.AreEqual(1, items.Length); - Assert.IsInstanceOf(items.First().Content); - } - - [Test] - public void OfType2() - { - var publishedSnapshot = GetPublishedSnapshot(); - var content = publishedSnapshot.Content.GetAtRoot() - .OfType() - .Distinct() - .ToIndexedArray(); - Assert.AreEqual(1, content.Length); - Assert.IsInstanceOf(content.First().Content); - } - - [Test] - public void OfType() - { - var content = GetContent(1173) - .Children(VariationContextAccessor) - .OfType() - .First(x => x.UmbracoNaviHide); - Assert.AreEqual(1176, content.Id); - } - - [Test] - public void Position() - { - var items = GetContent(1173).Children(VariationContextAccessor) - .Where(x => x.Value(Mock.Of(), "umbracoNaviHide") == 0) - .ToIndexedArray(); - - Assert.AreEqual(3, items.Length); - - Assert.IsTrue(items.First().IsFirst()); - Assert.IsFalse(items.First().IsLast()); - Assert.IsFalse(items.Skip(1).First().IsFirst()); - Assert.IsFalse(items.Skip(1).First().IsLast()); - Assert.IsFalse(items.Skip(2).First().IsFirst()); - Assert.IsTrue(items.Skip(2).First().IsLast()); - } - - private class ImageWithLegendModel : PublishedElement - { - public ImageWithLegendModel( - IPublishedContentType contentType, - Guid fragmentKey, - Dictionary values, - bool previewing) - : base(contentType, fragmentKey, values, previewing) - { - } - - public string Legend => this.Value(Mock.Of(), "legend"); - - public IPublishedContent Image => this.Value(Mock.Of(), "image"); - - public int Size => this.Value(Mock.Of(), "size"); - } - - // [PublishedModel("ContentType2")] - // public class ContentType2 : PublishedContentModel - // { - // #region Plumbing - - // public ContentType2(IPublishedContent content, IPublishedValueFallback fallback) - // : base(content, fallback) - // { } - - // #endregion - - // public int Prop1 => this.Value(Mock.Of(), "prop1"); - // } - - // [PublishedModel("ContentType2Sub")] - // public class ContentType2Sub : ContentType2 - // { - // #region Plumbing - - // public ContentType2Sub(IPublishedContent content, IPublishedValueFallback fallback) - // : base(content, fallback) - // { } - - // #endregion - // } -} +// using System.Collections.Generic; +// using System.Linq; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Core.PropertyEditors; +// using Umbraco.Cms.Core.PublishedCache; +// using Umbraco.Cms.Core.Strings; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Published; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class PublishedContentTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// var xml = PublishedContentXml.PublishedContentTestXml(1234, _node1173Guid); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// _dataTypes = dataTypes; +// +// // configure the Home content type to be composed of another for tests. +// var compositionType = new ContentType(TestHelper.ShortStringHelper, -1) { Alias = "MyCompositionAlias" }; +// contentTypes.First(x => x.Alias == "Home").AddContentType(compositionType); +// +// InitializedCache(kits, contentTypes, dataTypes); +// } +// +// private readonly Guid _node1173Guid = Guid.NewGuid(); +// private PublishedModelFactory _publishedModelFactory; +// private DataType[] _dataTypes; +// +// // override to specify our own factory with custom types +// protected override IPublishedModelFactory PublishedModelFactory +// => _publishedModelFactory ??= new PublishedModelFactory( +// new[] { typeof(Home), typeof(Anything), typeof(CustomDocument) }, +// PublishedValueFallback); +// +// [PublishedModel("Home")] +// internal class Home : PublishedContentModel +// { +// public Home(IPublishedContent content, IPublishedValueFallback fallback) +// : base(content, fallback) +// { +// } +// +// public bool UmbracoNaviHide => this.Value(Mock.Of(), "umbracoNaviHide"); +// } +// +// [PublishedModel("anything")] +// internal class Anything : PublishedContentModel +// { +// public Anything(IPublishedContent content, IPublishedValueFallback fallback) +// : base(content, fallback) +// { +// } +// } +// +// [PublishedModel("CustomDocument")] +// internal class CustomDocument : PublishedContentModel +// { +// public CustomDocument(IPublishedContent content, IPublishedValueFallback fallback) +// : base(content, fallback) +// { +// } +// } +// +// [Test] +// public void GetNodeByIds() +// { +// var snapshot = GetPublishedSnapshot(); +// +// var contentById = snapshot.Content.GetById(1173); +// Assert.IsNotNull(contentById); +// var contentByGuid = snapshot.Content.GetById(_node1173Guid); +// Assert.IsNotNull(contentByGuid); +// Assert.AreEqual(contentById.Id, contentByGuid.Id); +// Assert.AreEqual(contentById.Key, contentByGuid.Key); +// +// contentById = snapshot.Content.GetById(666); +// Assert.IsNull(contentById); +// contentByGuid = snapshot.Content.GetById(Guid.NewGuid()); +// Assert.IsNull(contentByGuid); +// } +// +// [Test] +// public void Is_Last_From_Where_Filter_Dynamic_Linq() +// { +// var doc = GetContent(1173); +// +// var items = doc.Children(VariationContextAccessor).Where(x => x.IsVisible(Mock.Of())) +// .ToIndexedArray(); +// +// foreach (var item in items) +// { +// if (item.Content.Id != 1178) +// { +// Assert.IsFalse(item.IsLast(), $"The item {item.Content.Id} is last"); +// } +// else +// { +// Assert.IsTrue(item.IsLast(), $"The item {item.Content.Id} is not last"); +// } +// } +// } +// +// [Test] +// public void Is_Last_From_Where_Filter() +// { +// var doc = GetContent(1173); +// +// var items = doc +// .Children(VariationContextAccessor) +// .Where(x => x.IsVisible(Mock.Of())) +// .ToIndexedArray(); +// +// Assert.AreEqual(4, items.Length); +// +// foreach (var d in items) +// { +// switch (d.Content.Id) +// { +// case 1174: +// Assert.IsTrue(d.IsFirst()); +// Assert.IsFalse(d.IsLast()); +// break; +// case 117: +// Assert.IsFalse(d.IsFirst()); +// Assert.IsFalse(d.IsLast()); +// break; +// case 1177: +// Assert.IsFalse(d.IsFirst()); +// Assert.IsFalse(d.IsLast()); +// break; +// case 1178: +// Assert.IsFalse(d.IsFirst()); +// Assert.IsTrue(d.IsLast()); +// break; +// default: +// Assert.Fail("Invalid id."); +// break; +// } +// } +// } +// +// [Test] +// public void Is_Last_From_Where_Filter2() +// { +// var doc = GetContent(1173); +// var ct = doc.ContentType; +// +// var items = doc.Children(VariationContextAccessor) +// .Select(x => x.CreateModel(PublishedModelFactory)) // linq, returns IEnumerable +// +// // only way around this is to make sure every IEnumerable extension +// // explicitely returns a PublishedContentSet, not an IEnumerable +// .OfType() // ours, return IEnumerable (actually a PublishedContentSet) +// .Where(x => x.IsVisible(Mock.Of())) // so, here it's linq again :-( +// .ToIndexedArray(); // so, we need that one for the test to pass +// +// Assert.AreEqual(1, items.Length); +// +// foreach (var d in items) +// { +// switch (d.Content.Id) +// { +// case 1174: +// Assert.IsTrue(d.IsFirst()); +// Assert.IsTrue(d.IsLast()); +// break; +// default: +// Assert.Fail("Invalid id."); +// break; +// } +// } +// } +// +// [Test] +// public void Is_Last_From_Take() +// { +// var doc = GetContent(1173); +// +// var items = doc.Children(VariationContextAccessor).Take(4).ToIndexedArray(); +// +// foreach (var item in items) +// { +// if (item.Content.Id != 1178) +// { +// Assert.IsFalse(item.IsLast()); +// } +// else +// { +// Assert.IsTrue(item.IsLast()); +// } +// } +// } +// +// [Test] +// public void Is_Last_From_Skip() +// { +// var doc = GetContent(1173); +// +// foreach (var d in doc.Children(VariationContextAccessor).Skip(1).ToIndexedArray()) +// { +// if (d.Content.Id != 1176) +// { +// Assert.IsFalse(d.IsLast()); +// } +// else +// { +// Assert.IsTrue(d.IsLast()); +// } +// } +// } +// +// [Test] +// public void Is_Last_From_Concat() +// { +// var doc = GetContent(1173); +// +// var items = doc.Children(VariationContextAccessor) +// .Concat(new[] { GetContent(1175), GetContent(4444) }) +// .ToIndexedArray(); +// +// foreach (var item in items) +// { +// if (item.Content.Id != 4444) +// { +// Assert.IsFalse(item.IsLast()); +// } +// else +// { +// Assert.IsTrue(item.IsLast()); +// } +// } +// } +// +// [Test] +// public void Descendants_Ordered_Properly() +// { +// var doc = GetContent(1046); +// +// var expected = new[] { 1046, 1173, 1174, 117, 1177, 1178, 1179, 1176, 1175, 4444, 1172 }; +// var exindex = 0; +// +// // must respect the XPath descendants-or-self axis! +// foreach (var d in doc.DescendantsOrSelf(Mock.Of())) +// { +// Assert.AreEqual(expected[exindex++], d.Id); +// } +// } +// +// [Test] +// public void Get_Property_Value_Recursive() +// { +// // TODO: We need to use a different fallback? +// var doc = GetContent(1174); +// var rVal = doc.Value(PublishedValueFallback, "testRecursive", fallback: Fallback.ToAncestors); +// var nullVal = doc.Value(PublishedValueFallback, "DoNotFindThis", fallback: Fallback.ToAncestors); +// Assert.AreEqual("This is the recursive val", rVal); +// Assert.AreEqual(null, nullVal); +// } +// +// [Test] +// public void Get_Property_Value_Uses_Converter() +// { +// var doc = GetContent(1173); +// +// var propVal = doc.Value(PublishedValueFallback, "content"); +// Assert.IsInstanceOf(typeof(IHtmlEncodedString), propVal); +// Assert.AreEqual("
This is some content
", propVal.ToString()); +// +// var propVal2 = doc.Value(PublishedValueFallback, "content"); +// Assert.IsInstanceOf(typeof(IHtmlEncodedString), propVal2); +// Assert.AreEqual("
This is some content
", propVal2.ToString()); +// +// var propVal3 = doc.Value(PublishedValueFallback, "Content"); +// Assert.IsInstanceOf(typeof(IHtmlEncodedString), propVal3); +// Assert.AreEqual("
This is some content
", propVal3.ToString()); +// } +// +// [Test] +// public void Complex_Linq() +// { +// var doc = GetContent(1173); +// +// var result = doc.Ancestors().OrderBy(x => x.Level) +// .Single() +// .Descendants(Mock.Of()) +// .FirstOrDefault(x => +// x.Value(PublishedValueFallback, "selectedNodes", fallback: Fallback.ToDefaultValue, defaultValue: string.Empty).Split(',').Contains("1173")); +// +// Assert.IsNotNull(result); +// } +// +// [Test] +// public void Children_GroupBy_DocumentTypeAlias() +// { +// // var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { }); +// // var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { }); +// // var contentTypes = new Dictionary +// // { +// // { home.Alias, home }, +// // { custom.Alias, custom } +// // }; +// // ContentTypesCache.GetPublishedContentTypeByAlias = alias => contentTypes[alias]; +// var doc = GetContent(1046); +// +// var found1 = doc.Children(VariationContextAccessor).GroupBy(x => x.ContentType.Alias).ToArray(); +// +// Assert.AreEqual(2, found1.Length); +// Assert.AreEqual(2, found1.Single(x => x.Key.ToString() == "Home").Count()); +// Assert.AreEqual(1, found1.Single(x => x.Key.ToString() == "CustomDocument").Count()); +// } +// +// [Test] +// public void Children_Where_DocumentTypeAlias() +// { +// // var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { }); +// // var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { }); +// // var contentTypes = new Dictionary +// // { +// // { home.Alias, home }, +// // { custom.Alias, custom } +// // }; +// // ContentTypesCache.GetPublishedContentTypeByAlias = alias => contentTypes[alias]; +// var doc = GetContent(1046); +// +// var found1 = doc.Children(VariationContextAccessor).Where(x => x.ContentType.Alias == "CustomDocument"); +// var found2 = doc.Children(VariationContextAccessor).Where(x => x.ContentType.Alias == "Home"); +// +// Assert.AreEqual(1, found1.Count()); +// Assert.AreEqual(2, found2.Count()); +// } +// +// [Test] +// public void Children_Order_By_Update_Date() +// { +// var doc = GetContent(1173); +// +// var ordered = doc.Children(VariationContextAccessor).OrderBy(x => x.UpdateDate); +// +// var correctOrder = new[] { 1178, 1177, 1174, 1176 }; +// for (var i = 0; i < correctOrder.Length; i++) +// { +// Assert.AreEqual(correctOrder[i], ordered.ElementAt(i).Id); +// } +// } +// +// [Test] +// public void FirstChild() +// { +// var doc = GetContent(1173); // has child nodes +// Assert.IsNotNull(doc.FirstChild(Mock.Of())); +// Assert.IsNotNull(doc.FirstChild(Mock.Of(), x => true)); +// Assert.IsNotNull(doc.FirstChild(Mock.Of())); +// +// doc = GetContent(1175); // does not have child nodes +// Assert.IsNull(doc.FirstChild(Mock.Of())); +// Assert.IsNull(doc.FirstChild(Mock.Of(), x => true)); +// Assert.IsNull(doc.FirstChild(Mock.Of())); +// } +// +// [Test] +// public void FirstChildAsT() +// { +// var doc = GetContent(1046); // has child nodes +// +// var model = doc.FirstChild(Mock.Of(), x => true); // predicate +// +// Assert.IsNotNull(model); +// Assert.IsTrue(model.Id == 1173); +// Assert.IsInstanceOf(model); +// Assert.IsInstanceOf(model); +// +// doc = GetContent(1175); // does not have child nodes +// Assert.IsNull(doc.FirstChild(Mock.Of())); +// Assert.IsNull(doc.FirstChild(Mock.Of(), x => true)); +// } +// +// [Test] +// public void IsComposedOf() +// { +// var doc = GetContent(1173); +// +// var isComposedOf = doc.IsComposedOf("MyCompositionAlias"); +// +// Assert.IsTrue(isComposedOf); +// } +// +// [Test] +// public void HasProperty() +// { +// var doc = GetContent(1173); +// +// var hasProp = doc.HasProperty(Constants.Conventions.Content.UrlAlias); +// +// Assert.IsTrue(hasProp); +// } +// +// [Test] +// public void HasValue() +// { +// var doc = GetContent(1173); +// +// var hasValue = doc.HasValue(Mock.Of(), Constants.Conventions.Content.UrlAlias); +// var noValue = doc.HasValue(Mock.Of(), "blahblahblah"); +// +// Assert.IsTrue(hasValue); +// Assert.IsFalse(noValue); +// } +// +// [Test] +// public void Ancestors_Where_Visible() +// { +// var doc = GetContent(1174); +// +// var whereVisible = doc.Ancestors().Where(x => x.IsVisible(Mock.Of())); +// Assert.AreEqual(1, whereVisible.Count()); +// } +// +// [Test] +// public void Visible() +// { +// var hidden = GetContent(1046); +// var visible = GetContent(1173); +// +// Assert.IsFalse(hidden.IsVisible(Mock.Of())); +// Assert.IsTrue(visible.IsVisible(Mock.Of())); +// } +// +// [Test] +// public void Ancestor_Or_Self() +// { +// var doc = GetContent(1173); +// +// var result = doc.AncestorOrSelf(); +// +// Assert.IsNotNull(result); +// +// // ancestor-or-self has to be self! +// Assert.AreEqual(1173, result.Id); +// } +// +// [Test] +// public void U4_4559() +// { +// var doc = GetContent(1174); +// var result = doc.AncestorOrSelf(1); +// Assert.IsNotNull(result); +// Assert.AreEqual(1046, result.Id); +// } +// +// [Test] +// public void Ancestors_Or_Self() +// { +// var doc = GetContent(1174); +// +// var result = doc.AncestorsOrSelf().ToArray(); +// +// Assert.IsNotNull(result); +// +// Assert.AreEqual(3, result.Length); +// Assert.IsTrue(result.Select(x => x.Id).ContainsAll(new[] { 1174, 1173, 1046 })); +// } +// +// [Test] +// public void Ancestors() +// { +// var doc = GetContent(1174); +// +// var result = doc.Ancestors().ToArray(); +// +// Assert.IsNotNull(result); +// +// Assert.AreEqual(2, result.Length); +// Assert.IsTrue(result.Select(x => x.Id).ContainsAll(new[] { 1173, 1046 })); +// } +// +// [Test] +// public void IsAncestor() +// { +// // Structure: +// // - Root : 1046 (no parent) +// // -- Home: 1173 (parent 1046) +// // -- Custom Doc: 1178 (parent 1173) +// // --- Custom Doc2: 1179 (parent: 1178) +// // -- Custom Doc4: 117 (parent 1173) +// // - Custom Doc3: 1172 (no parent) +// var home = GetContent(1173); +// var root = GetContent(1046); +// var customDoc = GetContent(1178); +// var customDoc2 = GetContent(1179); +// var customDoc3 = GetContent(1172); +// var customDoc4 = GetContent(117); +// +// Assert.IsTrue(root.IsAncestor(customDoc4)); +// Assert.IsFalse(root.IsAncestor(customDoc3)); +// Assert.IsTrue(root.IsAncestor(customDoc2)); +// Assert.IsTrue(root.IsAncestor(customDoc)); +// Assert.IsTrue(root.IsAncestor(home)); +// Assert.IsFalse(root.IsAncestor(root)); +// +// Assert.IsTrue(home.IsAncestor(customDoc4)); +// Assert.IsFalse(home.IsAncestor(customDoc3)); +// Assert.IsTrue(home.IsAncestor(customDoc2)); +// Assert.IsTrue(home.IsAncestor(customDoc)); +// Assert.IsFalse(home.IsAncestor(home)); +// Assert.IsFalse(home.IsAncestor(root)); +// +// Assert.IsFalse(customDoc.IsAncestor(customDoc4)); +// Assert.IsFalse(customDoc.IsAncestor(customDoc3)); +// Assert.IsTrue(customDoc.IsAncestor(customDoc2)); +// Assert.IsFalse(customDoc.IsAncestor(customDoc)); +// Assert.IsFalse(customDoc.IsAncestor(home)); +// Assert.IsFalse(customDoc.IsAncestor(root)); +// +// Assert.IsFalse(customDoc2.IsAncestor(customDoc4)); +// Assert.IsFalse(customDoc2.IsAncestor(customDoc3)); +// Assert.IsFalse(customDoc2.IsAncestor(customDoc2)); +// Assert.IsFalse(customDoc2.IsAncestor(customDoc)); +// Assert.IsFalse(customDoc2.IsAncestor(home)); +// Assert.IsFalse(customDoc2.IsAncestor(root)); +// +// Assert.IsFalse(customDoc3.IsAncestor(customDoc3)); +// } +// +// [Test] +// public void IsAncestorOrSelf() +// { +// // Structure: +// // - Root : 1046 (no parent) +// // -- Home: 1173 (parent 1046) +// // -- Custom Doc: 1178 (parent 1173) +// // --- Custom Doc2: 1179 (parent: 1178) +// // -- Custom Doc4: 117 (parent 1173) +// // - Custom Doc3: 1172 (no parent) +// var home = GetContent(1173); +// var root = GetContent(1046); +// var customDoc = GetContent(1178); +// var customDoc2 = GetContent(1179); +// var customDoc3 = GetContent(1172); +// var customDoc4 = GetContent(117); +// +// Assert.IsTrue(root.IsAncestorOrSelf(customDoc4)); +// Assert.IsFalse(root.IsAncestorOrSelf(customDoc3)); +// Assert.IsTrue(root.IsAncestorOrSelf(customDoc2)); +// Assert.IsTrue(root.IsAncestorOrSelf(customDoc)); +// Assert.IsTrue(root.IsAncestorOrSelf(home)); +// Assert.IsTrue(root.IsAncestorOrSelf(root)); +// +// Assert.IsTrue(home.IsAncestorOrSelf(customDoc4)); +// Assert.IsFalse(home.IsAncestorOrSelf(customDoc3)); +// Assert.IsTrue(home.IsAncestorOrSelf(customDoc2)); +// Assert.IsTrue(home.IsAncestorOrSelf(customDoc)); +// Assert.IsTrue(home.IsAncestorOrSelf(home)); +// Assert.IsFalse(home.IsAncestorOrSelf(root)); +// +// Assert.IsFalse(customDoc.IsAncestorOrSelf(customDoc4)); +// Assert.IsFalse(customDoc.IsAncestorOrSelf(customDoc3)); +// Assert.IsTrue(customDoc.IsAncestorOrSelf(customDoc2)); +// Assert.IsTrue(customDoc.IsAncestorOrSelf(customDoc)); +// Assert.IsFalse(customDoc.IsAncestorOrSelf(home)); +// Assert.IsFalse(customDoc.IsAncestorOrSelf(root)); +// +// Assert.IsFalse(customDoc2.IsAncestorOrSelf(customDoc4)); +// Assert.IsFalse(customDoc2.IsAncestorOrSelf(customDoc3)); +// Assert.IsTrue(customDoc2.IsAncestorOrSelf(customDoc2)); +// Assert.IsFalse(customDoc2.IsAncestorOrSelf(customDoc)); +// Assert.IsFalse(customDoc2.IsAncestorOrSelf(home)); +// Assert.IsFalse(customDoc2.IsAncestorOrSelf(root)); +// +// Assert.IsTrue(customDoc4.IsAncestorOrSelf(customDoc4)); +// Assert.IsTrue(customDoc3.IsAncestorOrSelf(customDoc3)); +// } +// +// [Test] +// public void Descendants_Or_Self() +// { +// var doc = GetContent(1046); +// +// var result = doc.DescendantsOrSelf(Mock.Of()).ToArray(); +// +// Assert.IsNotNull(result); +// +// Assert.AreEqual(10, result.Count()); +// Assert.IsTrue(result.Select(x => x.Id).ContainsAll(new[] { 1046, 1173, 1174, 1176, 1175 })); +// } +// +// [Test] +// public void Descendants() +// { +// var doc = GetContent(1046); +// +// var result = doc.Descendants(Mock.Of()).ToArray(); +// +// Assert.IsNotNull(result); +// +// Assert.AreEqual(9, result.Count()); +// Assert.IsTrue(result.Select(x => x.Id).ContainsAll(new[] { 1173, 1174, 1176, 1175, 4444 })); +// } +// +// [Test] +// public void IsDescendant() +// { +// // Structure: +// // - Root : 1046 (no parent) +// // -- Home: 1173 (parent 1046) +// // -- Custom Doc: 1178 (parent 1173) +// // --- Custom Doc2: 1179 (parent: 1178) +// // -- Custom Doc4: 117 (parent 1173) +// // - Custom Doc3: 1172 (no parent) +// var home = GetContent(1173); +// var root = GetContent(1046); +// var customDoc = GetContent(1178); +// var customDoc2 = GetContent(1179); +// var customDoc3 = GetContent(1172); +// var customDoc4 = GetContent(117); +// +// Assert.IsFalse(root.IsDescendant(root)); +// Assert.IsFalse(root.IsDescendant(home)); +// Assert.IsFalse(root.IsDescendant(customDoc)); +// Assert.IsFalse(root.IsDescendant(customDoc2)); +// Assert.IsFalse(root.IsDescendant(customDoc3)); +// Assert.IsFalse(root.IsDescendant(customDoc4)); +// +// Assert.IsTrue(home.IsDescendant(root)); +// Assert.IsFalse(home.IsDescendant(home)); +// Assert.IsFalse(home.IsDescendant(customDoc)); +// Assert.IsFalse(home.IsDescendant(customDoc2)); +// Assert.IsFalse(home.IsDescendant(customDoc3)); +// Assert.IsFalse(home.IsDescendant(customDoc4)); +// +// Assert.IsTrue(customDoc.IsDescendant(root)); +// Assert.IsTrue(customDoc.IsDescendant(home)); +// Assert.IsFalse(customDoc.IsDescendant(customDoc)); +// Assert.IsFalse(customDoc.IsDescendant(customDoc2)); +// Assert.IsFalse(customDoc.IsDescendant(customDoc3)); +// Assert.IsFalse(customDoc.IsDescendant(customDoc4)); +// +// Assert.IsTrue(customDoc2.IsDescendant(root)); +// Assert.IsTrue(customDoc2.IsDescendant(home)); +// Assert.IsTrue(customDoc2.IsDescendant(customDoc)); +// Assert.IsFalse(customDoc2.IsDescendant(customDoc2)); +// Assert.IsFalse(customDoc2.IsDescendant(customDoc3)); +// Assert.IsFalse(customDoc2.IsDescendant(customDoc4)); +// +// Assert.IsFalse(customDoc3.IsDescendant(customDoc3)); +// } +// +// [Test] +// public void IsDescendantOrSelf() +// { +// // Structure: +// // - Root : 1046 (no parent) +// // -- Home: 1173 (parent 1046) +// // -- Custom Doc: 1178 (parent 1173) +// // --- Custom Doc2: 1179 (parent: 1178) +// // -- Custom Doc4: 117 (parent 1173) +// // - Custom Doc3: 1172 (no parent) +// var home = GetContent(1173); +// var root = GetContent(1046); +// var customDoc = GetContent(1178); +// var customDoc2 = GetContent(1179); +// var customDoc3 = GetContent(1172); +// var customDoc4 = GetContent(117); +// +// Assert.IsTrue(root.IsDescendantOrSelf(root)); +// Assert.IsFalse(root.IsDescendantOrSelf(home)); +// Assert.IsFalse(root.IsDescendantOrSelf(customDoc)); +// Assert.IsFalse(root.IsDescendantOrSelf(customDoc2)); +// Assert.IsFalse(root.IsDescendantOrSelf(customDoc3)); +// Assert.IsFalse(root.IsDescendantOrSelf(customDoc4)); +// +// Assert.IsTrue(home.IsDescendantOrSelf(root)); +// Assert.IsTrue(home.IsDescendantOrSelf(home)); +// Assert.IsFalse(home.IsDescendantOrSelf(customDoc)); +// Assert.IsFalse(home.IsDescendantOrSelf(customDoc2)); +// Assert.IsFalse(home.IsDescendantOrSelf(customDoc3)); +// Assert.IsFalse(home.IsDescendantOrSelf(customDoc4)); +// +// Assert.IsTrue(customDoc.IsDescendantOrSelf(root)); +// Assert.IsTrue(customDoc.IsDescendantOrSelf(home)); +// Assert.IsTrue(customDoc.IsDescendantOrSelf(customDoc)); +// Assert.IsFalse(customDoc.IsDescendantOrSelf(customDoc2)); +// Assert.IsFalse(customDoc.IsDescendantOrSelf(customDoc3)); +// Assert.IsFalse(customDoc.IsDescendantOrSelf(customDoc4)); +// +// Assert.IsTrue(customDoc2.IsDescendantOrSelf(root)); +// Assert.IsTrue(customDoc2.IsDescendantOrSelf(home)); +// Assert.IsTrue(customDoc2.IsDescendantOrSelf(customDoc)); +// Assert.IsTrue(customDoc2.IsDescendantOrSelf(customDoc2)); +// Assert.IsFalse(customDoc2.IsDescendantOrSelf(customDoc3)); +// Assert.IsFalse(customDoc2.IsDescendantOrSelf(customDoc4)); +// +// Assert.IsTrue(customDoc3.IsDescendantOrSelf(customDoc3)); +// } +// +// [Test] +// public void SiblingsAndSelf() +// { +// // Structure: +// // - Root : 1046 (no parent) +// // -- Level1.1: 1173 (parent 1046) +// // --- Level1.1.1: 1174 (parent 1173) +// // --- Level1.1.2: 117 (parent 1173) +// // --- Level1.1.3: 1177 (parent 1173) +// // --- Level1.1.4: 1178 (parent 1173) +// // ---- Level1.1.4.1: 1179 (parent 1178) +// // --- Level1.1.5: 1176 (parent 1173) +// // -- Level1.2: 1175 (parent 1046) +// // -- Level1.3: 4444 (parent 1046) +// // - Root : 1172 (no parent) +// var root = GetContent(1046); +// var level1_1 = GetContent(1173); +// var level1_1_1 = GetContent(1174); +// var level1_1_2 = GetContent(117); +// var level1_1_3 = GetContent(1177); +// var level1_1_4 = GetContent(1178); +// var level1_1_5 = GetContent(1176); +// var level1_2 = GetContent(1175); +// var level1_3 = GetContent(4444); +// var root2 = GetContent(1172); +// +// var publishedSnapshot = GetPublishedSnapshot(); +// +// CollectionAssertAreEqual(new[] { root, root2 }, root.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); +// +// CollectionAssertAreEqual(new[] { level1_1, level1_2, level1_3 }, level1_1.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); +// CollectionAssertAreEqual(new[] { level1_1, level1_2, level1_3 }, level1_2.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); +// CollectionAssertAreEqual(new[] { level1_1, level1_2, level1_3 }, level1_3.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); +// +// CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_1.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); +// CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_2.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); +// CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_3.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); +// CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_4.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); +// CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_5.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor)); +// } +// +// [Test] +// public void Siblings() +// { +// // Structure: +// // - Root : 1046 (no parent) +// // -- Level1.1: 1173 (parent 1046) +// // --- Level1.1.1: 1174 (parent 1173) +// // --- Level1.1.2: 117 (parent 1173) +// // --- Level1.1.3: 1177 (parent 1173) +// // --- Level1.1.4: 1178 (parent 1173) +// // ---- Level1.1.4.1: 1179 (parent 1178) +// // --- Level1.1.5: 1176 (parent 1173) +// // -- Level1.2: 1175 (parent 1046) +// // -- Level1.3: 4444 (parent 1046) +// // - Root : 1172 (no parent) +// var root = GetContent(1046); +// var level1_1 = GetContent(1173); +// var level1_1_1 = GetContent(1174); +// var level1_1_2 = GetContent(117); +// var level1_1_3 = GetContent(1177); +// var level1_1_4 = GetContent(1178); +// var level1_1_5 = GetContent(1176); +// var level1_2 = GetContent(1175); +// var level1_3 = GetContent(4444); +// var root2 = GetContent(1172); +// +// var publishedSnapshot = GetPublishedSnapshot(); +// +// CollectionAssertAreEqual(new[] { root2 }, root.Siblings(publishedSnapshot, VariationContextAccessor)); +// +// CollectionAssertAreEqual(new[] { level1_2, level1_3 }, level1_1.Siblings(publishedSnapshot, VariationContextAccessor)); +// CollectionAssertAreEqual(new[] { level1_1, level1_3 }, level1_2.Siblings(publishedSnapshot, VariationContextAccessor)); +// CollectionAssertAreEqual(new[] { level1_1, level1_2 }, level1_3.Siblings(publishedSnapshot, VariationContextAccessor)); +// +// CollectionAssertAreEqual(new[] { level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_1.Siblings(publishedSnapshot, VariationContextAccessor)); +// CollectionAssertAreEqual(new[] { level1_1_1, level1_1_3, level1_1_4, level1_1_5 }, level1_1_2.Siblings(publishedSnapshot, VariationContextAccessor)); +// CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_4, level1_1_5 }, level1_1_3.Siblings(publishedSnapshot, VariationContextAccessor)); +// CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_5 }, level1_1_4.Siblings(publishedSnapshot, VariationContextAccessor)); +// CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4 }, level1_1_5.Siblings(publishedSnapshot, VariationContextAccessor)); +// } +// +// private void CollectionAssertAreEqual(IEnumerable expected, IEnumerable actual) +// where T : IPublishedContent +// { +// var e = expected.Select(x => x.Id).ToArray(); +// var a = actual.Select(x => x.Id).ToArray(); +// CollectionAssert.AreEquivalent(e, a, $"\nExpected:\n{string.Join(", ", e)}\n\nActual:\n{string.Join(", ", a)}"); +// } +// +// [Test] +// public void FragmentProperty() +// { +// IEnumerable CreatePropertyTypes(IPublishedContentType contentType) +// { +// yield return PublishedContentTypeFactory.CreatePropertyType(contentType, "detached", _dataTypes[0].Id); +// } +// +// var ct = PublishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes); +// var pt = ct.GetPropertyType("detached"); +// var prop = new PublishedElementPropertyBase(pt, null, false, PropertyCacheLevel.None, 5548); +// Assert.IsInstanceOf(prop.GetValue()); +// Assert.AreEqual(5548, prop.GetValue()); +// } +// +// [Test] +// public void Fragment2() +// { +// IEnumerable CreatePropertyTypes(IPublishedContentType contentType) +// { +// yield return PublishedContentTypeFactory.CreatePropertyType(contentType, "legend", _dataTypes[0].Id); +// yield return PublishedContentTypeFactory.CreatePropertyType(contentType, "image", _dataTypes[0].Id); +// yield return PublishedContentTypeFactory.CreatePropertyType(contentType, "size", _dataTypes[0].Id); +// } +// +// const string val1 = "boom bam"; +// const int val2 = 0; +// const int val3 = 666; +// +// var guid = Guid.NewGuid(); +// +// var ct = PublishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes); +// +// var c = new ImageWithLegendModel( +// ct, +// guid, +// new Dictionary { { "legend", val1 }, { "image", val2 }, { "size", val3 } }, +// false); +// +// Assert.AreEqual(val1, c.Legend); +// Assert.AreEqual(val3, c.Size); +// } +// +// [Test] +// public void First() +// { +// var publishedSnapshot = GetPublishedSnapshot(); +// var content = publishedSnapshot.Content.GetAtRoot().First(); +// Assert.AreEqual("Home", content.Name(VariationContextAccessor)); +// } +// +// [Test] +// public void Distinct() +// { +// var items = GetContent(1173) +// .Children(VariationContextAccessor) +// .Distinct() +// .Distinct() +// .ToIndexedArray(); +// +// Assert.AreEqual(5, items.Length); +// +// var item = items[0]; +// Assert.AreEqual(1174, item.Content.Id); +// Assert.IsTrue(item.IsFirst()); +// Assert.IsFalse(item.IsLast()); +// +// item = items[^1]; +// Assert.AreEqual(1176, item.Content.Id); +// Assert.IsFalse(item.IsFirst()); +// Assert.IsTrue(item.IsLast()); +// } +// +// [Test] +// public void OfType1() +// { +// var publishedSnapshot = GetPublishedSnapshot(); +// var items = publishedSnapshot.Content.GetAtRoot() +// .OfType() +// .Distinct() +// .ToIndexedArray(); +// Assert.AreEqual(1, items.Length); +// Assert.IsInstanceOf(items.First().Content); +// } +// +// [Test] +// public void OfType2() +// { +// var publishedSnapshot = GetPublishedSnapshot(); +// var content = publishedSnapshot.Content.GetAtRoot() +// .OfType() +// .Distinct() +// .ToIndexedArray(); +// Assert.AreEqual(1, content.Length); +// Assert.IsInstanceOf(content.First().Content); +// } +// +// [Test] +// public void OfType() +// { +// var content = GetContent(1173) +// .Children(VariationContextAccessor) +// .OfType() +// .First(x => x.UmbracoNaviHide); +// Assert.AreEqual(1176, content.Id); +// } +// +// [Test] +// public void Position() +// { +// var items = GetContent(1173).Children(VariationContextAccessor) +// .Where(x => x.Value(Mock.Of(), "umbracoNaviHide") == 0) +// .ToIndexedArray(); +// +// Assert.AreEqual(3, items.Length); +// +// Assert.IsTrue(items.First().IsFirst()); +// Assert.IsFalse(items.First().IsLast()); +// Assert.IsFalse(items.Skip(1).First().IsFirst()); +// Assert.IsFalse(items.Skip(1).First().IsLast()); +// Assert.IsFalse(items.Skip(2).First().IsFirst()); +// Assert.IsTrue(items.Skip(2).First().IsLast()); +// } +// +// private class ImageWithLegendModel : PublishedElement +// { +// public ImageWithLegendModel( +// IPublishedContentType contentType, +// Guid fragmentKey, +// Dictionary values, +// bool previewing) +// : base(contentType, fragmentKey, values, previewing) +// { +// } +// +// public string Legend => this.Value(Mock.Of(), "legend"); +// +// public IPublishedContent Image => this.Value(Mock.Of(), "image"); +// +// public int Size => this.Value(Mock.Of(), "size"); +// } +// +// // [PublishedModel("ContentType2")] +// // public class ContentType2 : PublishedContentModel +// // { +// // #region Plumbing +// +// // public ContentType2(IPublishedContent content, IPublishedValueFallback fallback) +// // : base(content, fallback) +// // { } +// +// // #endregion +// +// // public int Prop1 => this.Value(Mock.Of(), "prop1"); +// // } +// +// // [PublishedModel("ContentType2Sub")] +// // public class ContentType2Sub : ContentType2 +// // { +// // #region Plumbing +// +// // public ContentType2Sub(IPublishedContent content, IPublishedValueFallback fallback) +// // : base(content, fallback) +// // { } +// +// // #endregion +// // } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedMediaTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedMediaTests.cs index dfd94e624b..979618144f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedMediaTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedMediaTests.cs @@ -1,241 +1,242 @@ -using System.Collections.Generic; -using System.Linq; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.Serialization; -using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Builders.Extensions; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; - -/// -/// Tests the typed extension methods on IPublishedContent using the DefaultPublishedMediaStore -/// -[TestFixture] -public class PublishedMediaTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - var dataTypes = GetDefaultDataTypes().ToList(); - var serializer = new SystemTextConfigurationEditorJsonSerializer(); - var rteDataType = new DataType(new VoidEditor("RTE", Mock.Of()), serializer) { Id = 4 }; - dataTypes.Add(rteDataType); - _dataTypes = dataTypes.ToArray(); - - _propertyDataTypes = new Dictionary - { - // defaults will just use the first one - [string.Empty] = _dataTypes[0], - - // content uses the RTE - ["content"] = _dataTypes[1], - }; - } - - private Dictionary _propertyDataTypes; - private DataType[] _dataTypes; - - private ContentNodeKit CreateRoot(out MediaType mediaType) - { - mediaType = new MediaType(ShortStringHelper, -1); - - var item1Data = new ContentDataBuilder() - .WithName("Content 1") - .WithProperties(new PropertyDataBuilder() - .WithPropertyData("content", "
This is some content
") - .Build()) - - // build with a dynamically created media type - .Build(ShortStringHelper, _propertyDataTypes, mediaType, "image2"); - - var item1 = ContentNodeKitBuilder.CreateWithContent( - mediaType.Id, - 1, - "-1,1", - draftData: item1Data, - publishedData: item1Data); - - return item1; - } - - private IEnumerable CreateChildren( - int startId, - ContentNodeKit parent, - IMediaType mediaType, - int count) - { - for (var i = 0; i < count; i++) - { - var id = startId + i + 1; - - var item1Data = new ContentDataBuilder() - .WithName("Child " + id) - .WithProperties(new PropertyDataBuilder() - .WithPropertyData("content", "
This is some content
") - .Build()) - .Build(); - - var parentPath = parent.Node.Path; - - var item1 = ContentNodeKitBuilder.CreateWithContent( - mediaType.Id, - id, - $"{parentPath},{id}", - draftData: item1Data, - publishedData: item1Data); - - yield return item1; - } - } - - private void InitializeWithHierarchy( - out int rootId, - out IReadOnlyList firstLevelChildren, - out IReadOnlyList secondLevelChildren) - { - var cache = new List(); - var root = CreateRoot(out var mediaType); - firstLevelChildren = CreateChildren(10, root, mediaType, 3).ToList(); - secondLevelChildren = CreateChildren(20, firstLevelChildren[0], mediaType, 3).ToList(); - cache.Add(root); - cache.AddRange(firstLevelChildren); - cache.AddRange(secondLevelChildren); - InitializedCache(null, null, _dataTypes, cache, new[] { mediaType }); - rootId = root.Node.Id; - } - - [Test] - public void Get_Property_Value_Uses_Converter() - { - var cache = CreateRoot(out var mediaType); - InitializedCache(null, null, _dataTypes.ToArray(), new[] { cache }, new[] { mediaType }); - - var publishedMedia = GetMedia(1); - - var propVal = publishedMedia.Value(PublishedValueFallback, "content"); - Assert.IsInstanceOf(propVal); - Assert.AreEqual("
This is some content
", propVal.ToString()); - - var propVal2 = publishedMedia.Value(PublishedValueFallback, "content"); - Assert.IsInstanceOf(propVal2); - Assert.AreEqual("
This is some content
", propVal2.ToString()); - - var propVal3 = publishedMedia.Value(PublishedValueFallback, "Content"); - Assert.IsInstanceOf(propVal3); - Assert.AreEqual("
This is some content
", propVal3.ToString()); - } - - [Test] - public void Children() - { - InitializeWithHierarchy( - out var rootId, - out var firstLevelChildren, - out var secondLevelChildren); - - var publishedMedia = GetMedia(rootId); - - var rootChildren = publishedMedia.Children(VariationContextAccessor); - Assert.IsTrue(rootChildren.Select(x => x.Id).ContainsAll(firstLevelChildren.Select(x => x.Node.Id))); - - var publishedChild1 = GetMedia(firstLevelChildren[0].Node.Id); - var subChildren = publishedChild1.Children(VariationContextAccessor); - Assert.IsTrue(subChildren.Select(x => x.Id).ContainsAll(secondLevelChildren.Select(x => x.Node.Id))); - } - - [Test] - public void Descendants() - { - InitializeWithHierarchy( - out var rootId, - out var firstLevelChildren, - out var secondLevelChildren); - - var publishedMedia = GetMedia(rootId); - var rootDescendants = publishedMedia.Descendants(VariationContextAccessor); - - var descendentIds = - firstLevelChildren.Select(x => x.Node.Id).Concat(secondLevelChildren.Select(x => x.Node.Id)); - - Assert.IsTrue(rootDescendants.Select(x => x.Id).ContainsAll(descendentIds)); - - var publishedChild1 = GetMedia(firstLevelChildren[0].Node.Id); - var subDescendants = publishedChild1.Descendants(VariationContextAccessor); - Assert.IsTrue(subDescendants.Select(x => x.Id).ContainsAll(secondLevelChildren.Select(x => x.Node.Id))); - } - - [Test] - public void DescendantsOrSelf() - { - InitializeWithHierarchy( - out var rootId, - out var firstLevelChildren, - out var secondLevelChildren); - - var publishedMedia = GetMedia(rootId); - var rootDescendantsOrSelf = publishedMedia.DescendantsOrSelf(VariationContextAccessor); - var descendentAndSelfIds = firstLevelChildren.Select(x => x.Node.Id) - .Concat(secondLevelChildren.Select(x => x.Node.Id)) - .Append(rootId); - - Assert.IsTrue(rootDescendantsOrSelf.Select(x => x.Id).ContainsAll(descendentAndSelfIds)); - - var publishedChild1 = GetMedia(firstLevelChildren[0].Node.Id); - var subDescendantsOrSelf = publishedChild1.DescendantsOrSelf(VariationContextAccessor); - Assert.IsTrue(subDescendantsOrSelf.Select(x => x.Id).ContainsAll( - secondLevelChildren.Select(x => x.Node.Id).Append(firstLevelChildren[0].Node.Id))); - } - - [Test] - public void Parent() - { - InitializeWithHierarchy( - out var rootId, - out var firstLevelChildren, - out var secondLevelChildren); - - var publishedMedia = GetMedia(rootId); - Assert.AreEqual(null, publishedMedia.Parent); - - var publishedChild1 = GetMedia(firstLevelChildren[0].Node.Id); - Assert.AreEqual(publishedMedia.Id, publishedChild1.Parent.Id); - - var publishedSubChild1 = GetMedia(secondLevelChildren[0].Node.Id); - Assert.AreEqual(firstLevelChildren[0].Node.Id, publishedSubChild1.Parent.Id); - } - - [Test] - public void Ancestors() - { - InitializeWithHierarchy( - out var rootId, - out var firstLevelChildren, - out var secondLevelChildren); - - var publishedSubChild1 = GetMedia(secondLevelChildren[0].Node.Id); - Assert.IsTrue(publishedSubChild1.Ancestors().Select(x => x.Id) - .ContainsAll(new[] { firstLevelChildren[0].Node.Id, rootId })); - } - - [Test] - public void AncestorsOrSelf() - { - InitializeWithHierarchy( - out var rootId, - out var firstLevelChildren, - out var secondLevelChildren); - - var publishedSubChild1 = GetMedia(secondLevelChildren[0].Node.Id); - Assert.IsTrue(publishedSubChild1.AncestorsOrSelf().Select(x => x.Id) - .ContainsAll(new[] { secondLevelChildren[0].Node.Id, firstLevelChildren[0].Node.Id, rootId })); - } -} +// using System.Collections.Generic; +// using System.Linq; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.PropertyEditors; +// using Umbraco.Cms.Core.Strings; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Infrastructure.Serialization; +// using Umbraco.Cms.Tests.Common.Builders; +// using Umbraco.Cms.Tests.Common.Builders.Extensions; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; +// +// FIXME: Reintroduce if relevant +// /// +// /// Tests the typed extension methods on IPublishedContent using the DefaultPublishedMediaStore +// /// +// [TestFixture] +// public class PublishedMediaTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// var dataTypes = GetDefaultDataTypes().ToList(); +// var serializer = new SystemTextConfigurationEditorJsonSerializer(); +// var rteDataType = new DataType(new VoidEditor("RTE", Mock.Of()), serializer) { Id = 4 }; +// dataTypes.Add(rteDataType); +// _dataTypes = dataTypes.ToArray(); +// +// _propertyDataTypes = new Dictionary +// { +// // defaults will just use the first one +// [string.Empty] = _dataTypes[0], +// +// // content uses the RTE +// ["content"] = _dataTypes[1], +// }; +// } +// +// private Dictionary _propertyDataTypes; +// private DataType[] _dataTypes; +// +// private ContentNodeKit CreateRoot(out MediaType mediaType) +// { +// mediaType = new MediaType(ShortStringHelper, -1); +// +// var item1Data = new ContentDataBuilder() +// .WithName("Content 1") +// .WithProperties(new PropertyDataBuilder() +// .WithPropertyData("content", "
This is some content
") +// .Build()) +// +// // build with a dynamically created media type +// .Build(ShortStringHelper, _propertyDataTypes, mediaType, "image2"); +// +// var item1 = ContentNodeKitBuilder.CreateWithContent( +// mediaType.Id, +// 1, +// "-1,1", +// draftData: item1Data, +// publishedData: item1Data); +// +// return item1; +// } +// +// private IEnumerable CreateChildren( +// int startId, +// ContentNodeKit parent, +// IMediaType mediaType, +// int count) +// { +// for (var i = 0; i < count; i++) +// { +// var id = startId + i + 1; +// +// var item1Data = new ContentDataBuilder() +// .WithName("Child " + id) +// .WithProperties(new PropertyDataBuilder() +// .WithPropertyData("content", "
This is some content
") +// .Build()) +// .Build(); +// +// var parentPath = parent.Node.Path; +// +// var item1 = ContentNodeKitBuilder.CreateWithContent( +// mediaType.Id, +// id, +// $"{parentPath},{id}", +// draftData: item1Data, +// publishedData: item1Data); +// +// yield return item1; +// } +// } +// +// private void InitializeWithHierarchy( +// out int rootId, +// out IReadOnlyList firstLevelChildren, +// out IReadOnlyList secondLevelChildren) +// { +// var cache = new List(); +// var root = CreateRoot(out var mediaType); +// firstLevelChildren = CreateChildren(10, root, mediaType, 3).ToList(); +// secondLevelChildren = CreateChildren(20, firstLevelChildren[0], mediaType, 3).ToList(); +// cache.Add(root); +// cache.AddRange(firstLevelChildren); +// cache.AddRange(secondLevelChildren); +// InitializedCache(null, null, _dataTypes, cache, new[] { mediaType }); +// rootId = root.Node.Id; +// } +// +// [Test] +// public void Get_Property_Value_Uses_Converter() +// { +// var cache = CreateRoot(out var mediaType); +// InitializedCache(null, null, _dataTypes.ToArray(), new[] { cache }, new[] { mediaType }); +// +// var publishedMedia = GetMedia(1); +// +// var propVal = publishedMedia.Value(PublishedValueFallback, "content"); +// Assert.IsInstanceOf(propVal); +// Assert.AreEqual("
This is some content
", propVal.ToString()); +// +// var propVal2 = publishedMedia.Value(PublishedValueFallback, "content"); +// Assert.IsInstanceOf(propVal2); +// Assert.AreEqual("
This is some content
", propVal2.ToString()); +// +// var propVal3 = publishedMedia.Value(PublishedValueFallback, "Content"); +// Assert.IsInstanceOf(propVal3); +// Assert.AreEqual("
This is some content
", propVal3.ToString()); +// } +// +// [Test] +// public void Children() +// { +// InitializeWithHierarchy( +// out var rootId, +// out var firstLevelChildren, +// out var secondLevelChildren); +// +// var publishedMedia = GetMedia(rootId); +// +// var rootChildren = publishedMedia.Children(VariationContextAccessor); +// Assert.IsTrue(rootChildren.Select(x => x.Id).ContainsAll(firstLevelChildren.Select(x => x.Node.Id))); +// +// var publishedChild1 = GetMedia(firstLevelChildren[0].Node.Id); +// var subChildren = publishedChild1.Children(VariationContextAccessor); +// Assert.IsTrue(subChildren.Select(x => x.Id).ContainsAll(secondLevelChildren.Select(x => x.Node.Id))); +// } +// +// [Test] +// public void Descendants() +// { +// InitializeWithHierarchy( +// out var rootId, +// out var firstLevelChildren, +// out var secondLevelChildren); +// +// var publishedMedia = GetMedia(rootId); +// var rootDescendants = publishedMedia.Descendants(VariationContextAccessor); +// +// var descendentIds = +// firstLevelChildren.Select(x => x.Node.Id).Concat(secondLevelChildren.Select(x => x.Node.Id)); +// +// Assert.IsTrue(rootDescendants.Select(x => x.Id).ContainsAll(descendentIds)); +// +// var publishedChild1 = GetMedia(firstLevelChildren[0].Node.Id); +// var subDescendants = publishedChild1.Descendants(VariationContextAccessor); +// Assert.IsTrue(subDescendants.Select(x => x.Id).ContainsAll(secondLevelChildren.Select(x => x.Node.Id))); +// } +// +// [Test] +// public void DescendantsOrSelf() +// { +// InitializeWithHierarchy( +// out var rootId, +// out var firstLevelChildren, +// out var secondLevelChildren); +// +// var publishedMedia = GetMedia(rootId); +// var rootDescendantsOrSelf = publishedMedia.DescendantsOrSelf(VariationContextAccessor); +// var descendentAndSelfIds = firstLevelChildren.Select(x => x.Node.Id) +// .Concat(secondLevelChildren.Select(x => x.Node.Id)) +// .Append(rootId); +// +// Assert.IsTrue(rootDescendantsOrSelf.Select(x => x.Id).ContainsAll(descendentAndSelfIds)); +// +// var publishedChild1 = GetMedia(firstLevelChildren[0].Node.Id); +// var subDescendantsOrSelf = publishedChild1.DescendantsOrSelf(VariationContextAccessor); +// Assert.IsTrue(subDescendantsOrSelf.Select(x => x.Id).ContainsAll( +// secondLevelChildren.Select(x => x.Node.Id).Append(firstLevelChildren[0].Node.Id))); +// } +// +// [Test] +// public void Parent() +// { +// InitializeWithHierarchy( +// out var rootId, +// out var firstLevelChildren, +// out var secondLevelChildren); +// +// var publishedMedia = GetMedia(rootId); +// Assert.AreEqual(null, publishedMedia.Parent); +// +// var publishedChild1 = GetMedia(firstLevelChildren[0].Node.Id); +// Assert.AreEqual(publishedMedia.Id, publishedChild1.Parent.Id); +// +// var publishedSubChild1 = GetMedia(secondLevelChildren[0].Node.Id); +// Assert.AreEqual(firstLevelChildren[0].Node.Id, publishedSubChild1.Parent.Id); +// } +// +// [Test] +// public void Ancestors() +// { +// InitializeWithHierarchy( +// out var rootId, +// out var firstLevelChildren, +// out var secondLevelChildren); +// +// var publishedSubChild1 = GetMedia(secondLevelChildren[0].Node.Id); +// Assert.IsTrue(publishedSubChild1.Ancestors().Select(x => x.Id) +// .ContainsAll(new[] { firstLevelChildren[0].Node.Id, rootId })); +// } +// +// [Test] +// public void AncestorsOrSelf() +// { +// InitializeWithHierarchy( +// out var rootId, +// out var firstLevelChildren, +// out var secondLevelChildren); +// +// var publishedSubChild1 = GetMedia(secondLevelChildren[0].Node.Id); +// Assert.IsTrue(publishedSubChild1.AncestorsOrSelf().Select(x => x.Id) +// .ContainsAll(new[] { secondLevelChildren[0].Node.Id, firstLevelChildren[0].Node.Id, rootId })); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs index 796acb34dc..9955338c20 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs @@ -1,1345 +1,1346 @@ -using NUnit.Framework; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Services.Changes; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; - -[TestFixture] -public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - var propertyType = - new PropertyType(TestHelper.ShortStringHelper, "Umbraco.Void.Editor", ValueStorageType.Nvarchar) - { - Alias = "prop", - DataTypeId = 3, - Variations = ContentVariation.Nothing, - }; - _contentTypeInvariant = - new ContentType(TestHelper.ShortStringHelper, -1) - { - Id = 2, - Alias = "itype", - Variations = ContentVariation.Nothing, - }; - _contentTypeInvariant.AddPropertyType(propertyType); - - propertyType = - new PropertyType(TestHelper.ShortStringHelper, "Umbraco.Void.Editor", ValueStorageType.Nvarchar) - { - Alias = "prop", - DataTypeId = 3, - Variations = ContentVariation.Culture, - }; - _contentTypeVariant = - new ContentType(TestHelper.ShortStringHelper, -1) - { - Id = 3, - Alias = "vtype", - Variations = ContentVariation.Culture, - }; - _contentTypeVariant.AddPropertyType(propertyType); - - _contentTypes = new[] { _contentTypeInvariant, _contentTypeVariant }; - } - - private ContentType _contentTypeInvariant; - private ContentType _contentTypeVariant; - private ContentType[] _contentTypes; - - private IEnumerable GetNestedVariantKits() - { - var paths = new Dictionary { { -1, "-1" } }; - - // 1x variant (root) - yield return CreateVariantKit(1, -1, 1, paths); - - // 1x invariant under root - yield return CreateInvariantKit(4, 1, 1, paths); - - // 1x variant under root - yield return CreateVariantKit(7, 1, 4, paths); - - // 2x mixed under invariant - yield return CreateVariantKit(10, 4, 1, paths); - yield return CreateInvariantKit(11, 4, 2, paths); - - // 2x mixed under variant - yield return CreateVariantKit(12, 7, 1, paths); - yield return CreateInvariantKit(13, 7, 2, paths); - } - - private IEnumerable GetInvariantKits() - { - var paths = new Dictionary { { -1, "-1" } }; - - yield return CreateInvariantKit(1, -1, 1, paths); - yield return CreateInvariantKit(2, -1, 2, paths); - yield return CreateInvariantKit(3, -1, 3, paths); - - yield return CreateInvariantKit(4, 1, 1, paths); - yield return CreateInvariantKit(5, 1, 2, paths); - yield return CreateInvariantKit(6, 1, 3, paths); - - yield return CreateInvariantKit(7, 2, 3, paths); - yield return CreateInvariantKit(8, 2, 2, paths); - yield return CreateInvariantKit(9, 2, 1, paths); - - yield return CreateInvariantKit(10, 3, 1, paths); - - yield return CreateInvariantKit(11, 4, 1, paths); - yield return CreateInvariantKit(12, 4, 2, paths); - } - - private ContentNodeKit CreateInvariantKit(int id, int parentId, int sortOrder, Dictionary paths) - { - if (!paths.TryGetValue(parentId, out var parentPath)) - { - throw new Exception("Unknown parent."); - } - - var path = paths[id] = parentPath + "," + id; - var level = path.Count(x => x == ','); - var now = DateTime.Now; - - var contentData = ContentDataBuilder.CreateBasic("N" + id, now); - - return ContentNodeKitBuilder.CreateWithContent( - _contentTypeInvariant.Id, - id, - path, - sortOrder, - level, - parentId, - 0, - Guid.NewGuid(), - DateTime.Now, - null, - contentData); - } - - private IEnumerable GetVariantKits() - { - var paths = new Dictionary { { -1, "-1" } }; - - yield return CreateVariantKit(1, -1, 1, paths); - yield return CreateVariantKit(2, -1, 2, paths); - yield return CreateVariantKit(3, -1, 3, paths); - - yield return CreateVariantKit(4, 1, 1, paths); - yield return CreateVariantKit(5, 1, 2, paths); - yield return CreateVariantKit(6, 1, 3, paths); - - yield return CreateVariantKit(7, 2, 3, paths); - yield return CreateVariantKit(8, 2, 2, paths); - yield return CreateVariantKit(9, 2, 1, paths); - - yield return CreateVariantKit(10, 3, 1, paths); - - yield return CreateVariantKit(11, 4, 1, paths); - yield return CreateVariantKit(12, 4, 2, paths); - } - - private static Dictionary GetCultureInfos(int id, DateTime now) - { - var en = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; - var fr = new[] { 1, 3, 4, 6, 7, 9, 10, 12 }; - - var infos = new Dictionary(); - if (en.Contains(id)) - { - infos["en-US"] = new CultureVariation { Name = "N" + id + "-" + "en-US", Date = now, IsDraft = false }; - } - - if (fr.Contains(id)) - { - infos["fr-FR"] = new CultureVariation { Name = "N" + id + "-" + "fr-FR", Date = now, IsDraft = false }; - } - - return infos; - } - - private ContentNodeKit CreateVariantKit(int id, int parentId, int sortOrder, Dictionary paths) - { - if (!paths.TryGetValue(parentId, out var parentPath)) - { - throw new Exception("Unknown parent."); - } - - var path = paths[id] = parentPath + "," + id; - var level = path.Count(x => x == ','); - var now = DateTime.Now; - - var contentData = ContentDataBuilder.CreateVariant( - "N" + id, - GetCultureInfos(id, now), - now); - - return ContentNodeKitBuilder.CreateWithContent( - _contentTypeVariant.Id, - id, - path, - sortOrder, - level, - parentId, - draftData: null, - publishedData: contentData); - } - - private IEnumerable GetVariantWithDraftKits() - { - var paths = new Dictionary { { -1, "-1" } }; - - Dictionary GetCultureInfos(int id, DateTime now) - { - var en = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; - var fr = new[] { 1, 3, 4, 6, 7, 9, 10, 12 }; - - var infos = new Dictionary(); - if (en.Contains(id)) - { - infos["en-US"] = new CultureVariation { Name = "N" + id + "-" + "en-US", Date = now, IsDraft = false }; - } - - if (fr.Contains(id)) - { - infos["fr-FR"] = new CultureVariation { Name = "N" + id + "-" + "fr-FR", Date = now, IsDraft = false }; - } - - return infos; - } - - ContentNodeKit CreateKit(int id, int parentId, int sortOrder) - { - if (!paths.TryGetValue(parentId, out var parentPath)) - { - throw new Exception("Unknown parent."); - } - - var path = paths[id] = parentPath + "," + id; - var level = path.Count(x => x == ','); - var now = DateTime.Now; - - ContentData CreateContentData(bool published) - { - return ContentDataBuilder.CreateVariant( - "N" + id, - GetCultureInfos(id, now), - now, - published); - } - - var withDraft = id % 2 == 0; - var withPublished = !withDraft; - - return ContentNodeKitBuilder.CreateWithContent( - _contentTypeVariant.Id, - id, - path, - sortOrder, - level, - parentId, - draftData: withDraft ? CreateContentData(false) : null, - publishedData: withPublished ? CreateContentData(true) : null); - } - - yield return CreateKit(1, -1, 1); - yield return CreateKit(2, -1, 2); - yield return CreateKit(3, -1, 3); - - yield return CreateKit(4, 1, 1); - yield return CreateKit(5, 1, 2); - yield return CreateKit(6, 1, 3); - - yield return CreateKit(7, 2, 3); - yield return CreateKit(8, 2, 2); - yield return CreateKit(9, 2, 1); - - yield return CreateKit(10, 3, 1); - - yield return CreateKit(11, 4, 1); - yield return CreateKit(12, 4, 2); - } - - [Test] - public void EmptyTest() - { - InitializedCache(Array.Empty(), _contentTypes); - - var snapshot = GetPublishedSnapshot(); - - var documents = snapshot.Content.GetAtRoot().ToArray(); - Assert.AreEqual(0, documents.Length); - } - - [Test] - public void ChildrenTest() - { - InitializedCache(GetInvariantKits(), _contentTypes); - - var snapshot = GetPublishedSnapshot(); - - var documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N1", "N2", "N3"); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N4", "N5", "N6"); - - documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N9", "N8", "N7"); - - documents = snapshot.Content.GetById(3).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N10"); - - documents = snapshot.Content.GetById(4).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N11", "N12"); - - documents = snapshot.Content.GetById(10).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents); - } - - [Test] - public void ParentTest() - { - InitializedCache(GetInvariantKits(), _contentTypes); - - var snapshot = GetPublishedSnapshot(); - - Assert.IsNull(snapshot.Content.GetById(1).Parent); - Assert.IsNull(snapshot.Content.GetById(2).Parent); - Assert.IsNull(snapshot.Content.GetById(3).Parent); - - Assert.AreEqual(1, snapshot.Content.GetById(4).Parent?.Id); - Assert.AreEqual(1, snapshot.Content.GetById(5).Parent?.Id); - Assert.AreEqual(1, snapshot.Content.GetById(6).Parent?.Id); - - Assert.AreEqual(2, snapshot.Content.GetById(7).Parent?.Id); - Assert.AreEqual(2, snapshot.Content.GetById(8).Parent?.Id); - Assert.AreEqual(2, snapshot.Content.GetById(9).Parent?.Id); - - Assert.AreEqual(3, snapshot.Content.GetById(10).Parent?.Id); - - Assert.AreEqual(4, snapshot.Content.GetById(11).Parent?.Id); - Assert.AreEqual(4, snapshot.Content.GetById(12).Parent?.Id); - } - - [Test] - public void MoveToRootTest() - { - InitializedCache(GetInvariantKits(), _contentTypes); - - // get snapshot - var snapshot = GetPublishedSnapshot(); - - // do some changes - var kit = NuCacheContentService.ContentKits[10]; - NuCacheContentService.ContentKits[10] = ContentNodeKitBuilder.CreateWithContent( - _contentTypeInvariant.Id, - kit.Node.Id, - "-1,10", - 4, - 1, - -1, - draftData: null, - publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); - - // notify - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() - { - Id = 10, - ChangeTypes = TreeChangeTypes.RefreshBranch - } - }, - out _, - out _); - - // changes that *I* make are immediately visible on the current snapshot - var documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N1", "N2", "N3", "N10"); - - documents = snapshot.Content.GetById(3).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents); - - Assert.IsNull(snapshot.Content.GetById(10).Parent); - } - - [Test] - public void MoveFromRootTest() - { - InitializedCache(GetInvariantKits(), _contentTypes); - - // get snapshot - var snapshot = GetPublishedSnapshot(); - - // do some changes - var kit = NuCacheContentService.ContentKits[1]; - NuCacheContentService.ContentKits[1] = ContentNodeKitBuilder.CreateWithContent( - _contentTypeInvariant.Id, - kit.Node.Id, - "-1,3,10,1", - 1, - 1, - 10, - draftData: null, - publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); - - // notify - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() - { - Id = 1, - ChangeTypes = TreeChangeTypes.RefreshBranch - } - }, - out _, - out _); - - // changes that *I* make are immediately visible on the current snapshot - var documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N2", "N3"); - - documents = snapshot.Content.GetById(10).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N1"); - - Assert.AreEqual(10, snapshot.Content.GetById(1).Parent?.Id); - } - - [Test] - public void ReOrderTest() - { - InitializedCache(GetInvariantKits(), _contentTypes); - - // get snapshot - var snapshot = GetPublishedSnapshot(); - - // do some changes - var kit = NuCacheContentService.ContentKits[7]; - NuCacheContentService.ContentKits[7] = ContentNodeKitBuilder.CreateWithContent( - _contentTypeInvariant.Id, - kit.Node.Id, - kit.Node.Path, - 1, - kit.Node.Level, - kit.Node.ParentContentId, - draftData: null, - publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); - - kit = NuCacheContentService.ContentKits[8]; - NuCacheContentService.ContentKits[8] = ContentNodeKitBuilder.CreateWithContent( - _contentTypeInvariant.Id, - kit.Node.Id, - kit.Node.Path, - 3, - kit.Node.Level, - kit.Node.ParentContentId, - draftData: null, - publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); - - kit = NuCacheContentService.ContentKits[9]; - NuCacheContentService.ContentKits[9] = ContentNodeKitBuilder.CreateWithContent( - _contentTypeInvariant.Id, - kit.Node.Id, - kit.Node.Path, - 2, - kit.Node.Level, - kit.Node.ParentContentId, - draftData: null, - publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); - - // notify - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() - { - Id = kit.Node.ParentContentId, - ChangeTypes = TreeChangeTypes.RefreshBranch - } - }, - out _, - out _); - - // changes that *I* make are immediately visible on the current snapshot - var documents = snapshot.Content.GetById(kit.Node.ParentContentId).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N7", "N9", "N8"); - } - - [Test] - public void MoveTest() - { - InitializedCache(GetInvariantKits(), _contentTypes); - - // get snapshot - var snapshot = GetPublishedSnapshot(); - - // do some changes - var kit = NuCacheContentService.ContentKits[4]; - NuCacheContentService.ContentKits[4] = ContentNodeKitBuilder.CreateWithContent( - _contentTypeInvariant.Id, - kit.Node.Id, - kit.Node.Path, - 2, - kit.Node.Level, - kit.Node.ParentContentId, - draftData: null, - publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); - - kit = NuCacheContentService.ContentKits[5]; - NuCacheContentService.ContentKits[5] = ContentNodeKitBuilder.CreateWithContent( - _contentTypeInvariant.Id, - kit.Node.Id, - kit.Node.Path, - 3, - kit.Node.Level, - kit.Node.ParentContentId, - draftData: null, - publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); - - kit = NuCacheContentService.ContentKits[6]; - NuCacheContentService.ContentKits[6] = ContentNodeKitBuilder.CreateWithContent( - _contentTypeInvariant.Id, - kit.Node.Id, - kit.Node.Path, - 4, - kit.Node.Level, - kit.Node.ParentContentId, - draftData: null, - publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); - - kit = NuCacheContentService.ContentKits[7]; - NuCacheContentService.ContentKits[7] = ContentNodeKitBuilder.CreateWithContent( - _contentTypeInvariant.Id, - kit.Node.Id, - "-1,1,7", - 1, - kit.Node.Level, - 1, - draftData: null, - publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); - - // notify - SnapshotService.Notify( - new[] - { - // removal must come first - new ContentCacheRefresher.JsonPayload() - { - Id = 2, - ChangeTypes = TreeChangeTypes.RefreshBranch - }, - new ContentCacheRefresher.JsonPayload() - { - Id = 1, - ChangeTypes = TreeChangeTypes.RefreshBranch - } - }, - out _, - out _); - - // changes that *I* make are immediately visible on the current snapshot - var documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N7", "N4", "N5", "N6"); - - documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N9", "N8"); - - Assert.AreEqual(1, snapshot.Content.GetById(7).Parent?.Id); - } - - [Test] - public void Clear_Branch_Locked() - { - // This test replicates an issue we saw here https://github.com/umbraco/Umbraco-CMS/pull/7907#issuecomment-610259393 - // The data was sent to me and this replicates it's structure - var paths = new Dictionary { { -1, "-1" } }; - - InitializedCache( - new List - { - CreateInvariantKit(1, -1, 1, paths), // first level - CreateInvariantKit(2, 1, 1, paths), // second level - CreateInvariantKit(3, 2, 1, paths), // third level - - CreateInvariantKit(4, 3, 1, paths), // fourth level (we'll copy this one to the same level) - - CreateInvariantKit(5, 4, 1, paths), // 6th level - - CreateInvariantKit(6, 5, 2, paths), // 7th level - CreateInvariantKit(7, 5, 3, paths), - CreateInvariantKit(8, 5, 4, paths), - CreateInvariantKit(9, 5, 5, paths), - CreateInvariantKit(10, 5, 6, paths), - }, - _contentTypes); - - // get snapshot - var snapshot = GetPublishedSnapshot(); - - var snapshotService = (PublishedSnapshotService)SnapshotService; - var contentStore = snapshotService.GetContentStore(); - - // This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify) - contentStore.CreateSnapshot(); - - // notify - which ensures there are 2 generations in the cache meaning each LinkedNode has a Next value. - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() - { - Id = 4, - ChangeTypes = TreeChangeTypes.RefreshBranch - } - }, - out _, - out _); - - // refresh the branch again, this used to show the issue where a null ref exception would occur - // because in the ClearBranchLocked logic, when SetValueLocked was called within a recursive call - // to a child, we null out the .Value of the LinkedNode within the while loop because we didn't capture - // this value before recursing. - Assert.DoesNotThrow(() => - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() - { - Id = 4, - ChangeTypes = TreeChangeTypes.RefreshBranch - } - }, - out _, - out _)); - } - - [Test] - public void NestedVariationChildrenTest() - { - InitializedCache(GetNestedVariantKits(), _contentTypes); - - // get snapshot - var snapshot = GetPublishedSnapshot(); - - // TEST with en-us variation context - VariationContextAccessor.VariationContext = new VariationContext("en-US"); - - var documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N1-en-US"); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N4", "N7-en-US"); - - // Get the invariant and list children, there's a variation context so it should return invariant AND en-us variants - documents = snapshot.Content.GetById(4).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N10-en-US", "N11"); - - // Get the variant and list children, there's a variation context so it should return invariant AND en-us variants - documents = snapshot.Content.GetById(7).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N12-en-US", "N13"); - - // TEST with fr-fr variation context - VariationContextAccessor.VariationContext = new VariationContext("fr-FR"); - - documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N1-fr-FR"); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N4", "N7-fr-FR"); - - // Get the invariant and list children, there's a variation context so it should return invariant AND en-us variants - documents = snapshot.Content.GetById(4).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N10-fr-FR", "N11"); - - // Get the variant and list children, there's a variation context so it should return invariant AND en-us variants - documents = snapshot.Content.GetById(7).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N12-fr-FR", "N13"); - - // TEST specific cultures - documents = snapshot.Content.GetAtRoot("fr-FR").ToArray(); - AssertDocuments(documents, "N1-fr-FR"); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor, "fr-FR").ToArray(); - AssertDocuments(documents, "N4", "N7-fr-FR"); // NOTE: Returns invariant, this is expected - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor, string.Empty).ToArray(); - AssertDocuments(documents, "N4"); // Only returns invariant since that is what was requested - - documents = snapshot.Content.GetById(4).Children(VariationContextAccessor, "fr-FR").ToArray(); - AssertDocuments(documents, "N10-fr-FR", "N11"); // NOTE: Returns invariant, this is expected - documents = snapshot.Content.GetById(4).Children(VariationContextAccessor, string.Empty).ToArray(); - AssertDocuments(documents, "N11"); // Only returns invariant since that is what was requested - - documents = snapshot.Content.GetById(7).Children(VariationContextAccessor, "fr-FR").ToArray(); - AssertDocuments(documents, "N12-fr-FR", "N13"); // NOTE: Returns invariant, this is expected - documents = snapshot.Content.GetById(7).Children(VariationContextAccessor, string.Empty).ToArray(); - AssertDocuments(documents, "N13"); // Only returns invariant since that is what was requested - - // TEST without variation context - // This will actually convert the culture to "" which will be invariant since that's all it will know how to do - // This will return a NULL name for culture specific entities because there is no variation context - VariationContextAccessor.VariationContext = null; - - documents = snapshot.Content.GetAtRoot().ToArray(); - - // will return nothing because there's only variant at root - Assert.AreEqual(0, documents.Length); - - // so we'll continue to getting the known variant, do not fully assert this because the Name will NULL - documents = snapshot.Content.GetAtRoot("fr-FR").ToArray(); - Assert.AreEqual(1, documents.Length); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N4"); - - // Get the invariant and list children - documents = snapshot.Content.GetById(4).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N11"); - - // Get the variant and list children - documents = snapshot.Content.GetById(7).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N13"); - } - - [Test] - public void VariantChildrenTest() - { - InitializedCache(GetVariantKits(), _contentTypes); - - // get snapshot - var snapshot = GetPublishedSnapshot(); - - VariationContextAccessor.VariationContext = new VariationContext("en-US"); - - var documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N1-en-US", "N2-en-US", "N3-en-US"); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N4-en-US", "N5-en-US", "N6-en-US"); - - documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N9-en-US", "N8-en-US", "N7-en-US"); - - documents = snapshot.Content.GetById(3).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N10-en-US"); - - documents = snapshot.Content.GetById(4).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N11-en-US", "N12-en-US"); - - documents = snapshot.Content.GetById(10).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents); - - VariationContextAccessor.VariationContext = new VariationContext("fr-FR"); - - documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N1-fr-FR", "N3-fr-FR"); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N4-fr-FR", "N6-fr-FR"); - - documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N9-fr-FR", "N7-fr-FR"); - - documents = snapshot.Content.GetById(3).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N10-fr-FR"); - - documents = snapshot.Content.GetById(4).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N12-fr-FR"); - - documents = snapshot.Content.GetById(10).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor, "*").ToArray(); - AssertDocuments(documents, "N4-fr-FR", string.Empty, "N6-fr-FR"); - AssertDocuments("en-US", documents, "N4-en-US", "N5-en-US", "N6-en-US"); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor, "en-US").ToArray(); - AssertDocuments(documents, "N4-fr-FR", string.Empty, "N6-fr-FR"); - AssertDocuments("en-US", documents, "N4-en-US", "N5-en-US", "N6-en-US"); - - documents = snapshot.Content.GetById(1).ChildrenForAllCultures.ToArray(); - AssertDocuments(documents, "N4-fr-FR", string.Empty, "N6-fr-FR"); - AssertDocuments("en-US", documents, "N4-en-US", "N5-en-US", "N6-en-US"); - - documents = snapshot.Content.GetAtRoot("*").ToArray(); - AssertDocuments(documents, "N1-fr-FR", string.Empty, "N3-fr-FR"); - - documents = snapshot.Content.GetById(1).DescendantsOrSelf(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N1-fr-FR", "N4-fr-FR", "N12-fr-FR", "N6-fr-FR"); - - documents = snapshot.Content.GetById(1).DescendantsOrSelf(VariationContextAccessor, "*").ToArray(); - AssertDocuments(documents, "N1-fr-FR", "N4-fr-FR", string.Empty /*11*/, "N12-fr-FR", string.Empty /*5*/, "N6-fr-FR"); - } - - [Test] - public void RemoveTest() - { - InitializedCache(GetInvariantKits(), _contentTypes); - - // get snapshot - var snapshot = GetPublishedSnapshot(); - - var documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N1", "N2", "N3"); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N4", "N5", "N6"); - - documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N9", "N8", "N7"); - - // notify - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() // remove last - { - Id = 3, - ChangeTypes = TreeChangeTypes.Remove - }, - new ContentCacheRefresher.JsonPayload() // remove middle - { - Id = 5, - ChangeTypes = TreeChangeTypes.Remove - }, - new ContentCacheRefresher.JsonPayload() // remove first - { - Id = 9, - ChangeTypes = TreeChangeTypes.Remove - } - }, - out _, - out _); - - documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N1", "N2"); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N4", "N6"); - - documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N8", "N7"); - - // notify - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() // remove first - { - Id = 1, - ChangeTypes = TreeChangeTypes.Remove - }, - new ContentCacheRefresher.JsonPayload() // remove - { - Id = 8, - ChangeTypes = TreeChangeTypes.Remove - }, - new ContentCacheRefresher.JsonPayload() // remove - { - Id = 7, - ChangeTypes = TreeChangeTypes.Remove - } - }, - out _, - out _); - - documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N2"); - - documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents); - } - - [Test] - public void UpdateTest() - { - InitializedCache(GetInvariantKits(), _contentTypes); - - // get snapshot - var snapshot = GetPublishedSnapshot(); - - var snapshotService = (PublishedSnapshotService)SnapshotService; - var contentStore = snapshotService.GetContentStore(); - - var parentNodes = contentStore.Test.GetValues(1); - var parentNode = parentNodes[0]; - AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); - Assert.AreEqual(1, parentNode.gen); - - var documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N1", "N2", "N3"); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N4", "N5", "N6"); - - documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N9", "N8", "N7"); - - // notify - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() - { - Id = 1, - ChangeTypes = TreeChangeTypes.RefreshBranch - }, - new ContentCacheRefresher.JsonPayload() - { - Id = 2, - ChangeTypes = TreeChangeTypes.RefreshNode - } - }, - out _, - out _); - - parentNodes = contentStore.Test.GetValues(1); - Assert.AreEqual(2, parentNodes.Length); - parentNode = parentNodes[1]; // get the first gen - AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); // the structure should have remained the same - Assert.AreEqual(1, parentNode.gen); - parentNode = parentNodes[0]; // get the latest gen - AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); // the structure should have remained the same - Assert.AreEqual(2, parentNode.gen); - - documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N1", "N2", "N3"); - - documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N4", "N5", "N6"); - - documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); - AssertDocuments(documents, "N9", "N8", "N7"); - } - - [Test] - public void AtRootTest() - { - InitializedCache(GetVariantWithDraftKits(), _contentTypes); - - // get snapshot - var snapshot = GetPublishedSnapshot(); - - VariationContextAccessor.VariationContext = new VariationContext("en-US"); - - // N2 is draft only - var documents = snapshot.Content.GetAtRoot().ToArray(); - AssertDocuments(documents, "N1-en-US", /*"N2-en-US",*/ "N3-en-US"); - - documents = snapshot.Content.GetAtRoot(true).ToArray(); - AssertDocuments(documents, "N1-en-US", "N2-en-US", "N3-en-US"); - } - - [Test] - public void Set_All_Fast_Sorted_Ensure_LastChildContentId() - { - // see https://github.com/umbraco/Umbraco-CMS/issues/6353 - IEnumerable GetKits() - { - var paths = new Dictionary { { -1, "-1" } }; - - yield return CreateInvariantKit(1, -1, 1, paths); - yield return CreateInvariantKit(2, 1, 1, paths); - } - - InitializedCache(GetKits(), _contentTypes); - - var snapshotService = (PublishedSnapshotService)SnapshotService; - var contentStore = snapshotService.GetContentStore(); - - var parentNodes = contentStore.Test.GetValues(1); - var parentNode = parentNodes[0]; - AssertLinkedNode(parentNode.contentNode, -1, -1, -1, 2, 2); - - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() - { - Id = 2, - ChangeTypes = TreeChangeTypes.Remove - } - }, - out _, - out _); - - parentNodes = contentStore.Test.GetValues(1); - parentNode = parentNodes[0]; - - AssertLinkedNode(parentNode.contentNode, -1, -1, -1, -1, -1); - } - - [Test] - public void Remove_Node_Ensures_Linked_List() - { - // NOTE: these tests are not using real scopes, in which case a Scope does not control - // how the snapshots generations work. We are forcing new snapshot generations manually. - IEnumerable GetKits() - { - var paths = new Dictionary { { -1, "-1" } }; - - // root - yield return CreateInvariantKit(1, -1, 1, paths); - - // children - yield return CreateInvariantKit(2, 1, 1, paths); - yield return CreateInvariantKit(3, 1, 2, paths); // middle child - yield return CreateInvariantKit(4, 1, 3, paths); - } - - InitializedCache(GetKits(), _contentTypes); - - var snapshotService = (PublishedSnapshotService)SnapshotService; - var contentStore = snapshotService.GetContentStore(); - - Assert.AreEqual(1, contentStore.Test.LiveGen); - Assert.IsTrue(contentStore.Test.NextGen); - - var parentNode = contentStore.Test.GetValues(1)[0]; - Assert.AreEqual(1, parentNode.gen); - AssertLinkedNode(parentNode.contentNode, -1, -1, -1, 2, 4); - - var child1 = contentStore.Test.GetValues(2)[0]; - Assert.AreEqual(1, child1.gen); - AssertLinkedNode(child1.contentNode, 1, -1, 3, -1, -1); - - var child2 = contentStore.Test.GetValues(3)[0]; - Assert.AreEqual(1, child2.gen); - AssertLinkedNode(child2.contentNode, 1, 2, 4, -1, -1); - - var child3 = contentStore.Test.GetValues(4)[0]; - Assert.AreEqual(1, child3.gen); - AssertLinkedNode(child3.contentNode, 1, 3, -1, -1, -1); - - // This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify) - contentStore.CreateSnapshot(); - - Assert.IsFalse(contentStore.Test.NextGen); - - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() // remove middle child - { - Id = 3, - ChangeTypes = TreeChangeTypes.Remove - } - }, - out _, - out _); - - Assert.AreEqual(2, contentStore.Test.LiveGen); - Assert.IsTrue(contentStore.Test.NextGen); - - var parentNodes = contentStore.Test.GetValues(1); - Assert.AreEqual(1, parentNodes.Length); // the parent doesn't get changed, not new gen's are added - parentNode = parentNodes[0]; - Assert.AreEqual(1, parentNode.gen); // the parent node's gen has not changed - AssertLinkedNode(parentNode.contentNode, -1, -1, -1, 2, 4); - - child1 = contentStore.Test.GetValues(2)[0]; - Assert.AreEqual(2, child1.gen); // there is now 2x gen's of this item - AssertLinkedNode(child1.contentNode, 1, -1, 4, -1, -1); - - child2 = contentStore.Test.GetValues(3)[0]; - Assert.AreEqual(2, child2.gen); // there is now 2x gen's of this item - Assert.IsNull(child2.contentNode); // because it doesn't exist anymore - - child3 = contentStore.Test.GetValues(4)[0]; - Assert.AreEqual(2, child3.gen); // there is now 2x gen's of this item - AssertLinkedNode(child3.contentNode, 1, 2, -1, -1, -1); - } - - [Test] - public void Refresh_Node_Ensures_Linked_list() - { - // NOTE: these tests are not using real scopes, in which case a Scope does not control - // how the snapshots generations work. We are forcing new snapshot generations manually. - IEnumerable GetKits() - { - var paths = new Dictionary { { -1, "-1" } }; - - // root - yield return CreateInvariantKit(100, -1, 1, paths); - - // site - yield return CreateInvariantKit(2, 100, 1, paths); - yield return CreateInvariantKit(1, 100, 2, paths); // middle child - yield return CreateInvariantKit(3, 100, 3, paths); - - // children of 1 - yield return CreateInvariantKit(20, 1, 1, paths); - yield return CreateInvariantKit(30, 1, 2, paths); - yield return CreateInvariantKit(40, 1, 3, paths); - } - - InitializedCache(GetKits(), _contentTypes); - - var snapshotService = (PublishedSnapshotService)SnapshotService; - var contentStore = snapshotService.GetContentStore(); - - Assert.AreEqual(1, contentStore.Test.LiveGen); - Assert.IsTrue(contentStore.Test.NextGen); - - var middleNode = contentStore.Test.GetValues(1)[0]; - Assert.AreEqual(1, middleNode.gen); - AssertLinkedNode(middleNode.contentNode, 100, 2, 3, 20, 40); - - // This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify) - contentStore.CreateSnapshot(); - - Assert.IsFalse(contentStore.Test.NextGen); - - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() - { - Id = 1, - ChangeTypes = TreeChangeTypes.RefreshNode - } - }, - out _, - out _); - - Assert.AreEqual(2, contentStore.Test.LiveGen); - Assert.IsTrue(contentStore.Test.NextGen); - - middleNode = contentStore.Test.GetValues(1)[0]; - Assert.AreEqual(2, middleNode.gen); - AssertLinkedNode(middleNode.contentNode, 100, 2, 3, 20, 40); - } - - /// - /// This addresses issue: https://github.com/umbraco/Umbraco-CMS/issues/6698 - /// - /// - /// This test mimics if someone were to: - /// 1) Unpublish a "middle child" - /// 2) Save and publish it - /// 3) Publish it with descendants - /// 4) Repeat steps 2 and 3 - /// Which has caused an exception. To replicate this test: - /// 1) RefreshBranch with kits for a branch where the top most node is unpublished - /// 2) RefreshBranch with kits for the branch where the top most node is published - /// 3) RefreshBranch with kits for the branch where the top most node is published - /// 4) RefreshNode - /// 5) RefreshBranch with kits for the branch where the top most node is published - /// - [Test] - public void Refresh_Branch_With_Alternating_Publish_Flags() - { - // NOTE: these tests are not using real scopes, in which case a Scope does not control - // how the snapshots generations work. We are forcing new snapshot generations manually. - IEnumerable GetKits() - { - var paths = new Dictionary { { -1, "-1" } }; - - // root - yield return CreateInvariantKit(100, -1, 1, paths); - - // site - yield return CreateInvariantKit(2, 100, 1, paths); - yield return CreateInvariantKit(1, 100, 2, paths); // middle child - yield return CreateInvariantKit(3, 100, 3, paths); - - // children of 1 - yield return CreateInvariantKit(20, 1, 1, paths); - yield return CreateInvariantKit(30, 1, 2, paths); - yield return CreateInvariantKit(40, 1, 3, paths); - } - - // init with all published - InitializedCache(GetKits(), _contentTypes); - - var snapshotService = (PublishedSnapshotService)SnapshotService; - var contentStore = snapshotService.GetContentStore(); - - var rootKit = NuCacheContentService.ContentKits[1].Clone(PublishedModelFactory); - - void ChangePublishFlagOfRoot(bool published, int assertGen, TreeChangeTypes changeType) - { - // This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify) - contentStore.CreateSnapshot(); - - Assert.IsFalse(contentStore.Test.NextGen); - - // Change the root publish flag - var kit = rootKit.Clone( - PublishedModelFactory, - published ? null : rootKit.PublishedData, - published ? rootKit.PublishedData : null); - NuCacheContentService.ContentKits[1] = kit; - - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() - { - Id = 1, - ChangeTypes = changeType - } - }, - out _, - out _); - - Assert.AreEqual(assertGen, contentStore.Test.LiveGen); - Assert.IsTrue(contentStore.Test.NextGen); - - // get the latest gen for content Id 1 - var (gen, contentNode) = contentStore.Test.GetValues(1)[0]; - Assert.AreEqual(assertGen, gen); - - // even when unpublishing/re-publishing/etc... the linked list is always maintained - AssertLinkedNode(contentNode, 100, 2, 3, 20, 40); - } - - // unpublish the root - ChangePublishFlagOfRoot(false, 2, TreeChangeTypes.RefreshBranch); - - // publish the root (since it's not published, it will cause a RefreshBranch) - ChangePublishFlagOfRoot(true, 3, TreeChangeTypes.RefreshBranch); - - // publish root + descendants - ChangePublishFlagOfRoot(true, 4, TreeChangeTypes.RefreshBranch); - - // save/publish the root (since it's already published, it will just cause a RefreshNode - ChangePublishFlagOfRoot(true, 5, TreeChangeTypes.RefreshNode); - - // publish root + descendants - ChangePublishFlagOfRoot(true, 6, TreeChangeTypes.RefreshBranch); - } - - [Test] - public void Refresh_Branch_Ensures_Linked_List() - { - // NOTE: these tests are not using real scopes, in which case a Scope does not control - // how the snapshots generations work. We are forcing new snapshot generations manually. - IEnumerable GetKits() - { - var paths = new Dictionary { { -1, "-1" } }; - - // root - yield return CreateInvariantKit(1, -1, 1, paths); - - // children - yield return CreateInvariantKit(2, 1, 1, paths); - yield return CreateInvariantKit(3, 1, 2, paths); // middle child - yield return CreateInvariantKit(4, 1, 3, paths); - } - - InitializedCache(GetKits(), _contentTypes); - - var snapshotService = (PublishedSnapshotService)SnapshotService; - var contentStore = snapshotService.GetContentStore(); - - Assert.AreEqual(1, contentStore.Test.LiveGen); - Assert.IsTrue(contentStore.Test.NextGen); - - var parentNode = contentStore.Test.GetValues(1)[0]; - Assert.AreEqual(1, parentNode.gen); - AssertLinkedNode(parentNode.contentNode, -1, -1, -1, 2, 4); - - var child1 = contentStore.Test.GetValues(2)[0]; - Assert.AreEqual(1, child1.gen); - AssertLinkedNode(child1.contentNode, 1, -1, 3, -1, -1); - - var child2 = contentStore.Test.GetValues(3)[0]; - Assert.AreEqual(1, child2.gen); - AssertLinkedNode(child2.contentNode, 1, 2, 4, -1, -1); - - var child3 = contentStore.Test.GetValues(4)[0]; - Assert.AreEqual(1, child3.gen); - AssertLinkedNode(child3.contentNode, 1, 3, -1, -1, -1); - - // This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify) - contentStore.CreateSnapshot(); - - Assert.IsFalse(contentStore.Test.NextGen); - - SnapshotService.Notify( - new[] - { - new ContentCacheRefresher.JsonPayload() // remove middle child - { - Id = 3, - ChangeTypes = TreeChangeTypes.RefreshBranch - } - }, - out _, - out _); - - Assert.AreEqual(2, contentStore.Test.LiveGen); - Assert.IsTrue(contentStore.Test.NextGen); - - var parentNodes = contentStore.Test.GetValues(1); - Assert.AreEqual(1, parentNodes.Length); // the parent doesn't get changed, not new gen's are added - parentNode = parentNodes[0]; - Assert.AreEqual(1, parentNode.gen); // the parent node's gen has not changed - AssertLinkedNode(parentNode.contentNode, -1, -1, -1, 2, 4); - - child1 = contentStore.Test.GetValues(2)[0]; - Assert.AreEqual(2, child1.gen); // there is now 2x gen's of this item - AssertLinkedNode(child1.contentNode, 1, -1, 3, -1, -1); - - child2 = contentStore.Test.GetValues(3)[0]; - Assert.AreEqual(2, child2.gen); // there is now 2x gen's of this item - AssertLinkedNode(child2.contentNode, 1, 2, 4, -1, -1); - - child3 = contentStore.Test.GetValues(4)[0]; - Assert.AreEqual(2, child3.gen); // there is now 2x gen's of this item - AssertLinkedNode(child3.contentNode, 1, 3, -1, -1, -1); - } - - [Test] - public void MultipleCacheIteration() - { - // see https://github.com/umbraco/Umbraco-CMS/issues/7798 - InitializedCache(GetInvariantKits(), _contentTypes); - var snapshot = GetPublishedSnapshot(); - - var items = snapshot.Content.GetAtRoot().Where(x => x.ContentType.Alias == "itype").ToArray(); - Assert.AreEqual(items.Length, items.Length); - } - - private void AssertLinkedNode(ContentNode node, int parent, int prevSibling, int nextSibling, int firstChild, int lastChild) - { - Assert.AreEqual(parent, node.ParentContentId); - Assert.AreEqual(prevSibling, node.PreviousSiblingContentId); - Assert.AreEqual(nextSibling, node.NextSiblingContentId); - Assert.AreEqual(firstChild, node.FirstChildContentId); - Assert.AreEqual(lastChild, node.LastChildContentId); - } - - private void AssertDocuments(IPublishedContent[] documents, params string[] names) - { - Assert.AreEqual(names.Length, documents.Length); - for (var i = 0; i < names.Length; i++) - { - Assert.AreEqual(names[i], documents[i].Name); - } - } - - private void AssertDocuments(string culture, IPublishedContent[] documents, params string[] names) - { - Assert.AreEqual(names.Length, documents.Length); - for (var i = 0; i < names.Length; i++) - { - Assert.AreEqual(names[i], documents[i].Name(VariationContextAccessor, culture)); - } - } -} +// using NUnit.Framework; +// using Umbraco.Cms.Core.Cache; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Core.Services.Changes; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +// using Umbraco.Cms.Tests.Common.Builders; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// var propertyType = +// new PropertyType(TestHelper.ShortStringHelper, "Umbraco.Void.Editor", ValueStorageType.Nvarchar) +// { +// Alias = "prop", +// DataTypeId = 3, +// Variations = ContentVariation.Nothing, +// }; +// _contentTypeInvariant = +// new ContentType(TestHelper.ShortStringHelper, -1) +// { +// Id = 2, +// Alias = "itype", +// Variations = ContentVariation.Nothing, +// }; +// _contentTypeInvariant.AddPropertyType(propertyType); +// +// propertyType = +// new PropertyType(TestHelper.ShortStringHelper, "Umbraco.Void.Editor", ValueStorageType.Nvarchar) +// { +// Alias = "prop", +// DataTypeId = 3, +// Variations = ContentVariation.Culture, +// }; +// _contentTypeVariant = +// new ContentType(TestHelper.ShortStringHelper, -1) +// { +// Id = 3, +// Alias = "vtype", +// Variations = ContentVariation.Culture, +// }; +// _contentTypeVariant.AddPropertyType(propertyType); +// +// _contentTypes = new[] { _contentTypeInvariant, _contentTypeVariant }; +// } +// +// private ContentType _contentTypeInvariant; +// private ContentType _contentTypeVariant; +// private ContentType[] _contentTypes; +// +// private IEnumerable GetNestedVariantKits() +// { +// var paths = new Dictionary { { -1, "-1" } }; +// +// // 1x variant (root) +// yield return CreateVariantKit(1, -1, 1, paths); +// +// // 1x invariant under root +// yield return CreateInvariantKit(4, 1, 1, paths); +// +// // 1x variant under root +// yield return CreateVariantKit(7, 1, 4, paths); +// +// // 2x mixed under invariant +// yield return CreateVariantKit(10, 4, 1, paths); +// yield return CreateInvariantKit(11, 4, 2, paths); +// +// // 2x mixed under variant +// yield return CreateVariantKit(12, 7, 1, paths); +// yield return CreateInvariantKit(13, 7, 2, paths); +// } +// +// private IEnumerable GetInvariantKits() +// { +// var paths = new Dictionary { { -1, "-1" } }; +// +// yield return CreateInvariantKit(1, -1, 1, paths); +// yield return CreateInvariantKit(2, -1, 2, paths); +// yield return CreateInvariantKit(3, -1, 3, paths); +// +// yield return CreateInvariantKit(4, 1, 1, paths); +// yield return CreateInvariantKit(5, 1, 2, paths); +// yield return CreateInvariantKit(6, 1, 3, paths); +// +// yield return CreateInvariantKit(7, 2, 3, paths); +// yield return CreateInvariantKit(8, 2, 2, paths); +// yield return CreateInvariantKit(9, 2, 1, paths); +// +// yield return CreateInvariantKit(10, 3, 1, paths); +// +// yield return CreateInvariantKit(11, 4, 1, paths); +// yield return CreateInvariantKit(12, 4, 2, paths); +// } +// +// private ContentNodeKit CreateInvariantKit(int id, int parentId, int sortOrder, Dictionary paths) +// { +// if (!paths.TryGetValue(parentId, out var parentPath)) +// { +// throw new Exception("Unknown parent."); +// } +// +// var path = paths[id] = parentPath + "," + id; +// var level = path.Count(x => x == ','); +// var now = DateTime.Now; +// +// var contentData = ContentDataBuilder.CreateBasic("N" + id, now); +// +// return ContentNodeKitBuilder.CreateWithContent( +// _contentTypeInvariant.Id, +// id, +// path, +// sortOrder, +// level, +// parentId, +// 0, +// Guid.NewGuid(), +// DateTime.Now, +// null, +// contentData); +// } +// +// private IEnumerable GetVariantKits() +// { +// var paths = new Dictionary { { -1, "-1" } }; +// +// yield return CreateVariantKit(1, -1, 1, paths); +// yield return CreateVariantKit(2, -1, 2, paths); +// yield return CreateVariantKit(3, -1, 3, paths); +// +// yield return CreateVariantKit(4, 1, 1, paths); +// yield return CreateVariantKit(5, 1, 2, paths); +// yield return CreateVariantKit(6, 1, 3, paths); +// +// yield return CreateVariantKit(7, 2, 3, paths); +// yield return CreateVariantKit(8, 2, 2, paths); +// yield return CreateVariantKit(9, 2, 1, paths); +// +// yield return CreateVariantKit(10, 3, 1, paths); +// +// yield return CreateVariantKit(11, 4, 1, paths); +// yield return CreateVariantKit(12, 4, 2, paths); +// } +// +// private static Dictionary GetCultureInfos(int id, DateTime now) +// { +// var en = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; +// var fr = new[] { 1, 3, 4, 6, 7, 9, 10, 12 }; +// +// var infos = new Dictionary(); +// if (en.Contains(id)) +// { +// infos["en-US"] = new CultureVariation { Name = "N" + id + "-" + "en-US", Date = now, IsDraft = false }; +// } +// +// if (fr.Contains(id)) +// { +// infos["fr-FR"] = new CultureVariation { Name = "N" + id + "-" + "fr-FR", Date = now, IsDraft = false }; +// } +// +// return infos; +// } +// +// private ContentNodeKit CreateVariantKit(int id, int parentId, int sortOrder, Dictionary paths) +// { +// if (!paths.TryGetValue(parentId, out var parentPath)) +// { +// throw new Exception("Unknown parent."); +// } +// +// var path = paths[id] = parentPath + "," + id; +// var level = path.Count(x => x == ','); +// var now = DateTime.Now; +// +// var contentData = ContentDataBuilder.CreateVariant( +// "N" + id, +// GetCultureInfos(id, now), +// now); +// +// return ContentNodeKitBuilder.CreateWithContent( +// _contentTypeVariant.Id, +// id, +// path, +// sortOrder, +// level, +// parentId, +// draftData: null, +// publishedData: contentData); +// } +// +// private IEnumerable GetVariantWithDraftKits() +// { +// var paths = new Dictionary { { -1, "-1" } }; +// +// Dictionary GetCultureInfos(int id, DateTime now) +// { +// var en = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; +// var fr = new[] { 1, 3, 4, 6, 7, 9, 10, 12 }; +// +// var infos = new Dictionary(); +// if (en.Contains(id)) +// { +// infos["en-US"] = new CultureVariation { Name = "N" + id + "-" + "en-US", Date = now, IsDraft = false }; +// } +// +// if (fr.Contains(id)) +// { +// infos["fr-FR"] = new CultureVariation { Name = "N" + id + "-" + "fr-FR", Date = now, IsDraft = false }; +// } +// +// return infos; +// } +// +// ContentNodeKit CreateKit(int id, int parentId, int sortOrder) +// { +// if (!paths.TryGetValue(parentId, out var parentPath)) +// { +// throw new Exception("Unknown parent."); +// } +// +// var path = paths[id] = parentPath + "," + id; +// var level = path.Count(x => x == ','); +// var now = DateTime.Now; +// +// ContentData CreateContentData(bool published) +// { +// return ContentDataBuilder.CreateVariant( +// "N" + id, +// GetCultureInfos(id, now), +// now, +// published); +// } +// +// var withDraft = id % 2 == 0; +// var withPublished = !withDraft; +// +// return ContentNodeKitBuilder.CreateWithContent( +// _contentTypeVariant.Id, +// id, +// path, +// sortOrder, +// level, +// parentId, +// draftData: withDraft ? CreateContentData(false) : null, +// publishedData: withPublished ? CreateContentData(true) : null); +// } +// +// yield return CreateKit(1, -1, 1); +// yield return CreateKit(2, -1, 2); +// yield return CreateKit(3, -1, 3); +// +// yield return CreateKit(4, 1, 1); +// yield return CreateKit(5, 1, 2); +// yield return CreateKit(6, 1, 3); +// +// yield return CreateKit(7, 2, 3); +// yield return CreateKit(8, 2, 2); +// yield return CreateKit(9, 2, 1); +// +// yield return CreateKit(10, 3, 1); +// +// yield return CreateKit(11, 4, 1); +// yield return CreateKit(12, 4, 2); +// } +// +// [Test] +// public void EmptyTest() +// { +// InitializedCache(Array.Empty(), _contentTypes); +// +// var snapshot = GetPublishedSnapshot(); +// +// var documents = snapshot.Content.GetAtRoot().ToArray(); +// Assert.AreEqual(0, documents.Length); +// } +// +// [Test] +// public void ChildrenTest() +// { +// InitializedCache(GetInvariantKits(), _contentTypes); +// +// var snapshot = GetPublishedSnapshot(); +// +// var documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N1", "N2", "N3"); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N4", "N5", "N6"); +// +// documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N9", "N8", "N7"); +// +// documents = snapshot.Content.GetById(3).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N10"); +// +// documents = snapshot.Content.GetById(4).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N11", "N12"); +// +// documents = snapshot.Content.GetById(10).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents); +// } +// +// [Test] +// public void ParentTest() +// { +// InitializedCache(GetInvariantKits(), _contentTypes); +// +// var snapshot = GetPublishedSnapshot(); +// +// Assert.IsNull(snapshot.Content.GetById(1).Parent); +// Assert.IsNull(snapshot.Content.GetById(2).Parent); +// Assert.IsNull(snapshot.Content.GetById(3).Parent); +// +// Assert.AreEqual(1, snapshot.Content.GetById(4).Parent?.Id); +// Assert.AreEqual(1, snapshot.Content.GetById(5).Parent?.Id); +// Assert.AreEqual(1, snapshot.Content.GetById(6).Parent?.Id); +// +// Assert.AreEqual(2, snapshot.Content.GetById(7).Parent?.Id); +// Assert.AreEqual(2, snapshot.Content.GetById(8).Parent?.Id); +// Assert.AreEqual(2, snapshot.Content.GetById(9).Parent?.Id); +// +// Assert.AreEqual(3, snapshot.Content.GetById(10).Parent?.Id); +// +// Assert.AreEqual(4, snapshot.Content.GetById(11).Parent?.Id); +// Assert.AreEqual(4, snapshot.Content.GetById(12).Parent?.Id); +// } +// +// [Test] +// public void MoveToRootTest() +// { +// InitializedCache(GetInvariantKits(), _contentTypes); +// +// // get snapshot +// var snapshot = GetPublishedSnapshot(); +// +// // do some changes +// var kit = NuCacheContentService.ContentKits[10]; +// NuCacheContentService.ContentKits[10] = ContentNodeKitBuilder.CreateWithContent( +// _contentTypeInvariant.Id, +// kit.Node.Id, +// "-1,10", +// 4, +// 1, +// -1, +// draftData: null, +// publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); +// +// // notify +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() +// { +// Id = 10, +// ChangeTypes = TreeChangeTypes.RefreshBranch +// } +// }, +// out _, +// out _); +// +// // changes that *I* make are immediately visible on the current snapshot +// var documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N1", "N2", "N3", "N10"); +// +// documents = snapshot.Content.GetById(3).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents); +// +// Assert.IsNull(snapshot.Content.GetById(10).Parent); +// } +// +// [Test] +// public void MoveFromRootTest() +// { +// InitializedCache(GetInvariantKits(), _contentTypes); +// +// // get snapshot +// var snapshot = GetPublishedSnapshot(); +// +// // do some changes +// var kit = NuCacheContentService.ContentKits[1]; +// NuCacheContentService.ContentKits[1] = ContentNodeKitBuilder.CreateWithContent( +// _contentTypeInvariant.Id, +// kit.Node.Id, +// "-1,3,10,1", +// 1, +// 1, +// 10, +// draftData: null, +// publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); +// +// // notify +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() +// { +// Id = 1, +// ChangeTypes = TreeChangeTypes.RefreshBranch +// } +// }, +// out _, +// out _); +// +// // changes that *I* make are immediately visible on the current snapshot +// var documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N2", "N3"); +// +// documents = snapshot.Content.GetById(10).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N1"); +// +// Assert.AreEqual(10, snapshot.Content.GetById(1).Parent?.Id); +// } +// +// [Test] +// public void ReOrderTest() +// { +// InitializedCache(GetInvariantKits(), _contentTypes); +// +// // get snapshot +// var snapshot = GetPublishedSnapshot(); +// +// // do some changes +// var kit = NuCacheContentService.ContentKits[7]; +// NuCacheContentService.ContentKits[7] = ContentNodeKitBuilder.CreateWithContent( +// _contentTypeInvariant.Id, +// kit.Node.Id, +// kit.Node.Path, +// 1, +// kit.Node.Level, +// kit.Node.ParentContentId, +// draftData: null, +// publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); +// +// kit = NuCacheContentService.ContentKits[8]; +// NuCacheContentService.ContentKits[8] = ContentNodeKitBuilder.CreateWithContent( +// _contentTypeInvariant.Id, +// kit.Node.Id, +// kit.Node.Path, +// 3, +// kit.Node.Level, +// kit.Node.ParentContentId, +// draftData: null, +// publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); +// +// kit = NuCacheContentService.ContentKits[9]; +// NuCacheContentService.ContentKits[9] = ContentNodeKitBuilder.CreateWithContent( +// _contentTypeInvariant.Id, +// kit.Node.Id, +// kit.Node.Path, +// 2, +// kit.Node.Level, +// kit.Node.ParentContentId, +// draftData: null, +// publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); +// +// // notify +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() +// { +// Id = kit.Node.ParentContentId, +// ChangeTypes = TreeChangeTypes.RefreshBranch +// } +// }, +// out _, +// out _); +// +// // changes that *I* make are immediately visible on the current snapshot +// var documents = snapshot.Content.GetById(kit.Node.ParentContentId).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N7", "N9", "N8"); +// } +// +// [Test] +// public void MoveTest() +// { +// InitializedCache(GetInvariantKits(), _contentTypes); +// +// // get snapshot +// var snapshot = GetPublishedSnapshot(); +// +// // do some changes +// var kit = NuCacheContentService.ContentKits[4]; +// NuCacheContentService.ContentKits[4] = ContentNodeKitBuilder.CreateWithContent( +// _contentTypeInvariant.Id, +// kit.Node.Id, +// kit.Node.Path, +// 2, +// kit.Node.Level, +// kit.Node.ParentContentId, +// draftData: null, +// publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); +// +// kit = NuCacheContentService.ContentKits[5]; +// NuCacheContentService.ContentKits[5] = ContentNodeKitBuilder.CreateWithContent( +// _contentTypeInvariant.Id, +// kit.Node.Id, +// kit.Node.Path, +// 3, +// kit.Node.Level, +// kit.Node.ParentContentId, +// draftData: null, +// publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); +// +// kit = NuCacheContentService.ContentKits[6]; +// NuCacheContentService.ContentKits[6] = ContentNodeKitBuilder.CreateWithContent( +// _contentTypeInvariant.Id, +// kit.Node.Id, +// kit.Node.Path, +// 4, +// kit.Node.Level, +// kit.Node.ParentContentId, +// draftData: null, +// publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); +// +// kit = NuCacheContentService.ContentKits[7]; +// NuCacheContentService.ContentKits[7] = ContentNodeKitBuilder.CreateWithContent( +// _contentTypeInvariant.Id, +// kit.Node.Id, +// "-1,1,7", +// 1, +// kit.Node.Level, +// 1, +// draftData: null, +// publishedData: ContentDataBuilder.CreateBasic(kit.PublishedData.Name)); +// +// // notify +// SnapshotService.Notify( +// new[] +// { +// // removal must come first +// new ContentCacheRefresher.JsonPayload() +// { +// Id = 2, +// ChangeTypes = TreeChangeTypes.RefreshBranch +// }, +// new ContentCacheRefresher.JsonPayload() +// { +// Id = 1, +// ChangeTypes = TreeChangeTypes.RefreshBranch +// } +// }, +// out _, +// out _); +// +// // changes that *I* make are immediately visible on the current snapshot +// var documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N7", "N4", "N5", "N6"); +// +// documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N9", "N8"); +// +// Assert.AreEqual(1, snapshot.Content.GetById(7).Parent?.Id); +// } +// +// [Test] +// public void Clear_Branch_Locked() +// { +// // This test replicates an issue we saw here https://github.com/umbraco/Umbraco-CMS/pull/7907#issuecomment-610259393 +// // The data was sent to me and this replicates it's structure +// var paths = new Dictionary { { -1, "-1" } }; +// +// InitializedCache( +// new List +// { +// CreateInvariantKit(1, -1, 1, paths), // first level +// CreateInvariantKit(2, 1, 1, paths), // second level +// CreateInvariantKit(3, 2, 1, paths), // third level +// +// CreateInvariantKit(4, 3, 1, paths), // fourth level (we'll copy this one to the same level) +// +// CreateInvariantKit(5, 4, 1, paths), // 6th level +// +// CreateInvariantKit(6, 5, 2, paths), // 7th level +// CreateInvariantKit(7, 5, 3, paths), +// CreateInvariantKit(8, 5, 4, paths), +// CreateInvariantKit(9, 5, 5, paths), +// CreateInvariantKit(10, 5, 6, paths), +// }, +// _contentTypes); +// +// // get snapshot +// var snapshot = GetPublishedSnapshot(); +// +// var snapshotService = (PublishedSnapshotService)SnapshotService; +// var contentStore = snapshotService.GetContentStore(); +// +// // This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify) +// contentStore.CreateSnapshot(); +// +// // notify - which ensures there are 2 generations in the cache meaning each LinkedNode has a Next value. +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() +// { +// Id = 4, +// ChangeTypes = TreeChangeTypes.RefreshBranch +// } +// }, +// out _, +// out _); +// +// // refresh the branch again, this used to show the issue where a null ref exception would occur +// // because in the ClearBranchLocked logic, when SetValueLocked was called within a recursive call +// // to a child, we null out the .Value of the LinkedNode within the while loop because we didn't capture +// // this value before recursing. +// Assert.DoesNotThrow(() => +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() +// { +// Id = 4, +// ChangeTypes = TreeChangeTypes.RefreshBranch +// } +// }, +// out _, +// out _)); +// } +// +// [Test] +// public void NestedVariationChildrenTest() +// { +// InitializedCache(GetNestedVariantKits(), _contentTypes); +// +// // get snapshot +// var snapshot = GetPublishedSnapshot(); +// +// // TEST with en-us variation context +// VariationContextAccessor.VariationContext = new VariationContext("en-US"); +// +// var documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N1-en-US"); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N4", "N7-en-US"); +// +// // Get the invariant and list children, there's a variation context so it should return invariant AND en-us variants +// documents = snapshot.Content.GetById(4).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N10-en-US", "N11"); +// +// // Get the variant and list children, there's a variation context so it should return invariant AND en-us variants +// documents = snapshot.Content.GetById(7).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N12-en-US", "N13"); +// +// // TEST with fr-fr variation context +// VariationContextAccessor.VariationContext = new VariationContext("fr-FR"); +// +// documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N1-fr-FR"); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N4", "N7-fr-FR"); +// +// // Get the invariant and list children, there's a variation context so it should return invariant AND en-us variants +// documents = snapshot.Content.GetById(4).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N10-fr-FR", "N11"); +// +// // Get the variant and list children, there's a variation context so it should return invariant AND en-us variants +// documents = snapshot.Content.GetById(7).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N12-fr-FR", "N13"); +// +// // TEST specific cultures +// documents = snapshot.Content.GetAtRoot("fr-FR").ToArray(); +// AssertDocuments(documents, "N1-fr-FR"); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor, "fr-FR").ToArray(); +// AssertDocuments(documents, "N4", "N7-fr-FR"); // NOTE: Returns invariant, this is expected +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor, string.Empty).ToArray(); +// AssertDocuments(documents, "N4"); // Only returns invariant since that is what was requested +// +// documents = snapshot.Content.GetById(4).Children(VariationContextAccessor, "fr-FR").ToArray(); +// AssertDocuments(documents, "N10-fr-FR", "N11"); // NOTE: Returns invariant, this is expected +// documents = snapshot.Content.GetById(4).Children(VariationContextAccessor, string.Empty).ToArray(); +// AssertDocuments(documents, "N11"); // Only returns invariant since that is what was requested +// +// documents = snapshot.Content.GetById(7).Children(VariationContextAccessor, "fr-FR").ToArray(); +// AssertDocuments(documents, "N12-fr-FR", "N13"); // NOTE: Returns invariant, this is expected +// documents = snapshot.Content.GetById(7).Children(VariationContextAccessor, string.Empty).ToArray(); +// AssertDocuments(documents, "N13"); // Only returns invariant since that is what was requested +// +// // TEST without variation context +// // This will actually convert the culture to "" which will be invariant since that's all it will know how to do +// // This will return a NULL name for culture specific entities because there is no variation context +// VariationContextAccessor.VariationContext = null; +// +// documents = snapshot.Content.GetAtRoot().ToArray(); +// +// // will return nothing because there's only variant at root +// Assert.AreEqual(0, documents.Length); +// +// // so we'll continue to getting the known variant, do not fully assert this because the Name will NULL +// documents = snapshot.Content.GetAtRoot("fr-FR").ToArray(); +// Assert.AreEqual(1, documents.Length); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N4"); +// +// // Get the invariant and list children +// documents = snapshot.Content.GetById(4).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N11"); +// +// // Get the variant and list children +// documents = snapshot.Content.GetById(7).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N13"); +// } +// +// [Test] +// public void VariantChildrenTest() +// { +// InitializedCache(GetVariantKits(), _contentTypes); +// +// // get snapshot +// var snapshot = GetPublishedSnapshot(); +// +// VariationContextAccessor.VariationContext = new VariationContext("en-US"); +// +// var documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N1-en-US", "N2-en-US", "N3-en-US"); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N4-en-US", "N5-en-US", "N6-en-US"); +// +// documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N9-en-US", "N8-en-US", "N7-en-US"); +// +// documents = snapshot.Content.GetById(3).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N10-en-US"); +// +// documents = snapshot.Content.GetById(4).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N11-en-US", "N12-en-US"); +// +// documents = snapshot.Content.GetById(10).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents); +// +// VariationContextAccessor.VariationContext = new VariationContext("fr-FR"); +// +// documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N1-fr-FR", "N3-fr-FR"); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N4-fr-FR", "N6-fr-FR"); +// +// documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N9-fr-FR", "N7-fr-FR"); +// +// documents = snapshot.Content.GetById(3).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N10-fr-FR"); +// +// documents = snapshot.Content.GetById(4).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N12-fr-FR"); +// +// documents = snapshot.Content.GetById(10).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor, "*").ToArray(); +// AssertDocuments(documents, "N4-fr-FR", string.Empty, "N6-fr-FR"); +// AssertDocuments("en-US", documents, "N4-en-US", "N5-en-US", "N6-en-US"); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor, "en-US").ToArray(); +// AssertDocuments(documents, "N4-fr-FR", string.Empty, "N6-fr-FR"); +// AssertDocuments("en-US", documents, "N4-en-US", "N5-en-US", "N6-en-US"); +// +// documents = snapshot.Content.GetById(1).ChildrenForAllCultures.ToArray(); +// AssertDocuments(documents, "N4-fr-FR", string.Empty, "N6-fr-FR"); +// AssertDocuments("en-US", documents, "N4-en-US", "N5-en-US", "N6-en-US"); +// +// documents = snapshot.Content.GetAtRoot("*").ToArray(); +// AssertDocuments(documents, "N1-fr-FR", string.Empty, "N3-fr-FR"); +// +// documents = snapshot.Content.GetById(1).DescendantsOrSelf(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N1-fr-FR", "N4-fr-FR", "N12-fr-FR", "N6-fr-FR"); +// +// documents = snapshot.Content.GetById(1).DescendantsOrSelf(VariationContextAccessor, "*").ToArray(); +// AssertDocuments(documents, "N1-fr-FR", "N4-fr-FR", string.Empty /*11*/, "N12-fr-FR", string.Empty /*5*/, "N6-fr-FR"); +// } +// +// [Test] +// public void RemoveTest() +// { +// InitializedCache(GetInvariantKits(), _contentTypes); +// +// // get snapshot +// var snapshot = GetPublishedSnapshot(); +// +// var documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N1", "N2", "N3"); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N4", "N5", "N6"); +// +// documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N9", "N8", "N7"); +// +// // notify +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() // remove last +// { +// Id = 3, +// ChangeTypes = TreeChangeTypes.Remove +// }, +// new ContentCacheRefresher.JsonPayload() // remove middle +// { +// Id = 5, +// ChangeTypes = TreeChangeTypes.Remove +// }, +// new ContentCacheRefresher.JsonPayload() // remove first +// { +// Id = 9, +// ChangeTypes = TreeChangeTypes.Remove +// } +// }, +// out _, +// out _); +// +// documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N1", "N2"); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N4", "N6"); +// +// documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N8", "N7"); +// +// // notify +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() // remove first +// { +// Id = 1, +// ChangeTypes = TreeChangeTypes.Remove +// }, +// new ContentCacheRefresher.JsonPayload() // remove +// { +// Id = 8, +// ChangeTypes = TreeChangeTypes.Remove +// }, +// new ContentCacheRefresher.JsonPayload() // remove +// { +// Id = 7, +// ChangeTypes = TreeChangeTypes.Remove +// } +// }, +// out _, +// out _); +// +// documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N2"); +// +// documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents); +// } +// +// [Test] +// public void UpdateTest() +// { +// InitializedCache(GetInvariantKits(), _contentTypes); +// +// // get snapshot +// var snapshot = GetPublishedSnapshot(); +// +// var snapshotService = (PublishedSnapshotService)SnapshotService; +// var contentStore = snapshotService.GetContentStore(); +// +// var parentNodes = contentStore.Test.GetValues(1); +// var parentNode = parentNodes[0]; +// AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); +// Assert.AreEqual(1, parentNode.gen); +// +// var documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N1", "N2", "N3"); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N4", "N5", "N6"); +// +// documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N9", "N8", "N7"); +// +// // notify +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() +// { +// Id = 1, +// ChangeTypes = TreeChangeTypes.RefreshBranch +// }, +// new ContentCacheRefresher.JsonPayload() +// { +// Id = 2, +// ChangeTypes = TreeChangeTypes.RefreshNode +// } +// }, +// out _, +// out _); +// +// parentNodes = contentStore.Test.GetValues(1); +// Assert.AreEqual(2, parentNodes.Length); +// parentNode = parentNodes[1]; // get the first gen +// AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); // the structure should have remained the same +// Assert.AreEqual(1, parentNode.gen); +// parentNode = parentNodes[0]; // get the latest gen +// AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); // the structure should have remained the same +// Assert.AreEqual(2, parentNode.gen); +// +// documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N1", "N2", "N3"); +// +// documents = snapshot.Content.GetById(1).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N4", "N5", "N6"); +// +// documents = snapshot.Content.GetById(2).Children(VariationContextAccessor).ToArray(); +// AssertDocuments(documents, "N9", "N8", "N7"); +// } +// +// [Test] +// public void AtRootTest() +// { +// InitializedCache(GetVariantWithDraftKits(), _contentTypes); +// +// // get snapshot +// var snapshot = GetPublishedSnapshot(); +// +// VariationContextAccessor.VariationContext = new VariationContext("en-US"); +// +// // N2 is draft only +// var documents = snapshot.Content.GetAtRoot().ToArray(); +// AssertDocuments(documents, "N1-en-US", /*"N2-en-US",*/ "N3-en-US"); +// +// documents = snapshot.Content.GetAtRoot(true).ToArray(); +// AssertDocuments(documents, "N1-en-US", "N2-en-US", "N3-en-US"); +// } +// +// [Test] +// public void Set_All_Fast_Sorted_Ensure_LastChildContentId() +// { +// // see https://github.com/umbraco/Umbraco-CMS/issues/6353 +// IEnumerable GetKits() +// { +// var paths = new Dictionary { { -1, "-1" } }; +// +// yield return CreateInvariantKit(1, -1, 1, paths); +// yield return CreateInvariantKit(2, 1, 1, paths); +// } +// +// InitializedCache(GetKits(), _contentTypes); +// +// var snapshotService = (PublishedSnapshotService)SnapshotService; +// var contentStore = snapshotService.GetContentStore(); +// +// var parentNodes = contentStore.Test.GetValues(1); +// var parentNode = parentNodes[0]; +// AssertLinkedNode(parentNode.contentNode, -1, -1, -1, 2, 2); +// +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() +// { +// Id = 2, +// ChangeTypes = TreeChangeTypes.Remove +// } +// }, +// out _, +// out _); +// +// parentNodes = contentStore.Test.GetValues(1); +// parentNode = parentNodes[0]; +// +// AssertLinkedNode(parentNode.contentNode, -1, -1, -1, -1, -1); +// } +// +// [Test] +// public void Remove_Node_Ensures_Linked_List() +// { +// // NOTE: these tests are not using real scopes, in which case a Scope does not control +// // how the snapshots generations work. We are forcing new snapshot generations manually. +// IEnumerable GetKits() +// { +// var paths = new Dictionary { { -1, "-1" } }; +// +// // root +// yield return CreateInvariantKit(1, -1, 1, paths); +// +// // children +// yield return CreateInvariantKit(2, 1, 1, paths); +// yield return CreateInvariantKit(3, 1, 2, paths); // middle child +// yield return CreateInvariantKit(4, 1, 3, paths); +// } +// +// InitializedCache(GetKits(), _contentTypes); +// +// var snapshotService = (PublishedSnapshotService)SnapshotService; +// var contentStore = snapshotService.GetContentStore(); +// +// Assert.AreEqual(1, contentStore.Test.LiveGen); +// Assert.IsTrue(contentStore.Test.NextGen); +// +// var parentNode = contentStore.Test.GetValues(1)[0]; +// Assert.AreEqual(1, parentNode.gen); +// AssertLinkedNode(parentNode.contentNode, -1, -1, -1, 2, 4); +// +// var child1 = contentStore.Test.GetValues(2)[0]; +// Assert.AreEqual(1, child1.gen); +// AssertLinkedNode(child1.contentNode, 1, -1, 3, -1, -1); +// +// var child2 = contentStore.Test.GetValues(3)[0]; +// Assert.AreEqual(1, child2.gen); +// AssertLinkedNode(child2.contentNode, 1, 2, 4, -1, -1); +// +// var child3 = contentStore.Test.GetValues(4)[0]; +// Assert.AreEqual(1, child3.gen); +// AssertLinkedNode(child3.contentNode, 1, 3, -1, -1, -1); +// +// // This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify) +// contentStore.CreateSnapshot(); +// +// Assert.IsFalse(contentStore.Test.NextGen); +// +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() // remove middle child +// { +// Id = 3, +// ChangeTypes = TreeChangeTypes.Remove +// } +// }, +// out _, +// out _); +// +// Assert.AreEqual(2, contentStore.Test.LiveGen); +// Assert.IsTrue(contentStore.Test.NextGen); +// +// var parentNodes = contentStore.Test.GetValues(1); +// Assert.AreEqual(1, parentNodes.Length); // the parent doesn't get changed, not new gen's are added +// parentNode = parentNodes[0]; +// Assert.AreEqual(1, parentNode.gen); // the parent node's gen has not changed +// AssertLinkedNode(parentNode.contentNode, -1, -1, -1, 2, 4); +// +// child1 = contentStore.Test.GetValues(2)[0]; +// Assert.AreEqual(2, child1.gen); // there is now 2x gen's of this item +// AssertLinkedNode(child1.contentNode, 1, -1, 4, -1, -1); +// +// child2 = contentStore.Test.GetValues(3)[0]; +// Assert.AreEqual(2, child2.gen); // there is now 2x gen's of this item +// Assert.IsNull(child2.contentNode); // because it doesn't exist anymore +// +// child3 = contentStore.Test.GetValues(4)[0]; +// Assert.AreEqual(2, child3.gen); // there is now 2x gen's of this item +// AssertLinkedNode(child3.contentNode, 1, 2, -1, -1, -1); +// } +// +// [Test] +// public void Refresh_Node_Ensures_Linked_list() +// { +// // NOTE: these tests are not using real scopes, in which case a Scope does not control +// // how the snapshots generations work. We are forcing new snapshot generations manually. +// IEnumerable GetKits() +// { +// var paths = new Dictionary { { -1, "-1" } }; +// +// // root +// yield return CreateInvariantKit(100, -1, 1, paths); +// +// // site +// yield return CreateInvariantKit(2, 100, 1, paths); +// yield return CreateInvariantKit(1, 100, 2, paths); // middle child +// yield return CreateInvariantKit(3, 100, 3, paths); +// +// // children of 1 +// yield return CreateInvariantKit(20, 1, 1, paths); +// yield return CreateInvariantKit(30, 1, 2, paths); +// yield return CreateInvariantKit(40, 1, 3, paths); +// } +// +// InitializedCache(GetKits(), _contentTypes); +// +// var snapshotService = (PublishedSnapshotService)SnapshotService; +// var contentStore = snapshotService.GetContentStore(); +// +// Assert.AreEqual(1, contentStore.Test.LiveGen); +// Assert.IsTrue(contentStore.Test.NextGen); +// +// var middleNode = contentStore.Test.GetValues(1)[0]; +// Assert.AreEqual(1, middleNode.gen); +// AssertLinkedNode(middleNode.contentNode, 100, 2, 3, 20, 40); +// +// // This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify) +// contentStore.CreateSnapshot(); +// +// Assert.IsFalse(contentStore.Test.NextGen); +// +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() +// { +// Id = 1, +// ChangeTypes = TreeChangeTypes.RefreshNode +// } +// }, +// out _, +// out _); +// +// Assert.AreEqual(2, contentStore.Test.LiveGen); +// Assert.IsTrue(contentStore.Test.NextGen); +// +// middleNode = contentStore.Test.GetValues(1)[0]; +// Assert.AreEqual(2, middleNode.gen); +// AssertLinkedNode(middleNode.contentNode, 100, 2, 3, 20, 40); +// } +// +// /// +// /// This addresses issue: https://github.com/umbraco/Umbraco-CMS/issues/6698 +// /// +// /// +// /// This test mimics if someone were to: +// /// 1) Unpublish a "middle child" +// /// 2) Save and publish it +// /// 3) Publish it with descendants +// /// 4) Repeat steps 2 and 3 +// /// Which has caused an exception. To replicate this test: +// /// 1) RefreshBranch with kits for a branch where the top most node is unpublished +// /// 2) RefreshBranch with kits for the branch where the top most node is published +// /// 3) RefreshBranch with kits for the branch where the top most node is published +// /// 4) RefreshNode +// /// 5) RefreshBranch with kits for the branch where the top most node is published +// /// +// [Test] +// public void Refresh_Branch_With_Alternating_Publish_Flags() +// { +// // NOTE: these tests are not using real scopes, in which case a Scope does not control +// // how the snapshots generations work. We are forcing new snapshot generations manually. +// IEnumerable GetKits() +// { +// var paths = new Dictionary { { -1, "-1" } }; +// +// // root +// yield return CreateInvariantKit(100, -1, 1, paths); +// +// // site +// yield return CreateInvariantKit(2, 100, 1, paths); +// yield return CreateInvariantKit(1, 100, 2, paths); // middle child +// yield return CreateInvariantKit(3, 100, 3, paths); +// +// // children of 1 +// yield return CreateInvariantKit(20, 1, 1, paths); +// yield return CreateInvariantKit(30, 1, 2, paths); +// yield return CreateInvariantKit(40, 1, 3, paths); +// } +// +// // init with all published +// InitializedCache(GetKits(), _contentTypes); +// +// var snapshotService = (PublishedSnapshotService)SnapshotService; +// var contentStore = snapshotService.GetContentStore(); +// +// var rootKit = NuCacheContentService.ContentKits[1].Clone(PublishedModelFactory); +// +// void ChangePublishFlagOfRoot(bool published, int assertGen, TreeChangeTypes changeType) +// { +// // This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify) +// contentStore.CreateSnapshot(); +// +// Assert.IsFalse(contentStore.Test.NextGen); +// +// // Change the root publish flag +// var kit = rootKit.Clone( +// PublishedModelFactory, +// published ? null : rootKit.PublishedData, +// published ? rootKit.PublishedData : null); +// NuCacheContentService.ContentKits[1] = kit; +// +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() +// { +// Id = 1, +// ChangeTypes = changeType +// } +// }, +// out _, +// out _); +// +// Assert.AreEqual(assertGen, contentStore.Test.LiveGen); +// Assert.IsTrue(contentStore.Test.NextGen); +// +// // get the latest gen for content Id 1 +// var (gen, contentNode) = contentStore.Test.GetValues(1)[0]; +// Assert.AreEqual(assertGen, gen); +// +// // even when unpublishing/re-publishing/etc... the linked list is always maintained +// AssertLinkedNode(contentNode, 100, 2, 3, 20, 40); +// } +// +// // unpublish the root +// ChangePublishFlagOfRoot(false, 2, TreeChangeTypes.RefreshBranch); +// +// // publish the root (since it's not published, it will cause a RefreshBranch) +// ChangePublishFlagOfRoot(true, 3, TreeChangeTypes.RefreshBranch); +// +// // publish root + descendants +// ChangePublishFlagOfRoot(true, 4, TreeChangeTypes.RefreshBranch); +// +// // save/publish the root (since it's already published, it will just cause a RefreshNode +// ChangePublishFlagOfRoot(true, 5, TreeChangeTypes.RefreshNode); +// +// // publish root + descendants +// ChangePublishFlagOfRoot(true, 6, TreeChangeTypes.RefreshBranch); +// } +// +// [Test] +// public void Refresh_Branch_Ensures_Linked_List() +// { +// // NOTE: these tests are not using real scopes, in which case a Scope does not control +// // how the snapshots generations work. We are forcing new snapshot generations manually. +// IEnumerable GetKits() +// { +// var paths = new Dictionary { { -1, "-1" } }; +// +// // root +// yield return CreateInvariantKit(1, -1, 1, paths); +// +// // children +// yield return CreateInvariantKit(2, 1, 1, paths); +// yield return CreateInvariantKit(3, 1, 2, paths); // middle child +// yield return CreateInvariantKit(4, 1, 3, paths); +// } +// +// InitializedCache(GetKits(), _contentTypes); +// +// var snapshotService = (PublishedSnapshotService)SnapshotService; +// var contentStore = snapshotService.GetContentStore(); +// +// Assert.AreEqual(1, contentStore.Test.LiveGen); +// Assert.IsTrue(contentStore.Test.NextGen); +// +// var parentNode = contentStore.Test.GetValues(1)[0]; +// Assert.AreEqual(1, parentNode.gen); +// AssertLinkedNode(parentNode.contentNode, -1, -1, -1, 2, 4); +// +// var child1 = contentStore.Test.GetValues(2)[0]; +// Assert.AreEqual(1, child1.gen); +// AssertLinkedNode(child1.contentNode, 1, -1, 3, -1, -1); +// +// var child2 = contentStore.Test.GetValues(3)[0]; +// Assert.AreEqual(1, child2.gen); +// AssertLinkedNode(child2.contentNode, 1, 2, 4, -1, -1); +// +// var child3 = contentStore.Test.GetValues(4)[0]; +// Assert.AreEqual(1, child3.gen); +// AssertLinkedNode(child3.contentNode, 1, 3, -1, -1, -1); +// +// // This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify) +// contentStore.CreateSnapshot(); +// +// Assert.IsFalse(contentStore.Test.NextGen); +// +// SnapshotService.Notify( +// new[] +// { +// new ContentCacheRefresher.JsonPayload() // remove middle child +// { +// Id = 3, +// ChangeTypes = TreeChangeTypes.RefreshBranch +// } +// }, +// out _, +// out _); +// +// Assert.AreEqual(2, contentStore.Test.LiveGen); +// Assert.IsTrue(contentStore.Test.NextGen); +// +// var parentNodes = contentStore.Test.GetValues(1); +// Assert.AreEqual(1, parentNodes.Length); // the parent doesn't get changed, not new gen's are added +// parentNode = parentNodes[0]; +// Assert.AreEqual(1, parentNode.gen); // the parent node's gen has not changed +// AssertLinkedNode(parentNode.contentNode, -1, -1, -1, 2, 4); +// +// child1 = contentStore.Test.GetValues(2)[0]; +// Assert.AreEqual(2, child1.gen); // there is now 2x gen's of this item +// AssertLinkedNode(child1.contentNode, 1, -1, 3, -1, -1); +// +// child2 = contentStore.Test.GetValues(3)[0]; +// Assert.AreEqual(2, child2.gen); // there is now 2x gen's of this item +// AssertLinkedNode(child2.contentNode, 1, 2, 4, -1, -1); +// +// child3 = contentStore.Test.GetValues(4)[0]; +// Assert.AreEqual(2, child3.gen); // there is now 2x gen's of this item +// AssertLinkedNode(child3.contentNode, 1, 3, -1, -1, -1); +// } +// +// [Test] +// public void MultipleCacheIteration() +// { +// // see https://github.com/umbraco/Umbraco-CMS/issues/7798 +// InitializedCache(GetInvariantKits(), _contentTypes); +// var snapshot = GetPublishedSnapshot(); +// +// var items = snapshot.Content.GetAtRoot().Where(x => x.ContentType.Alias == "itype").ToArray(); +// Assert.AreEqual(items.Length, items.Length); +// } +// +// private void AssertLinkedNode(ContentNode node, int parent, int prevSibling, int nextSibling, int firstChild, int lastChild) +// { +// Assert.AreEqual(parent, node.ParentContentId); +// Assert.AreEqual(prevSibling, node.PreviousSiblingContentId); +// Assert.AreEqual(nextSibling, node.NextSiblingContentId); +// Assert.AreEqual(firstChild, node.FirstChildContentId); +// Assert.AreEqual(lastChild, node.LastChildContentId); +// } +// +// private void AssertDocuments(IPublishedContent[] documents, params string[] names) +// { +// Assert.AreEqual(names.Length, documents.Length); +// for (var i = 0; i < names.Length; i++) +// { +// Assert.AreEqual(names[i], documents[i].Name); +// } +// } +// +// private void AssertDocuments(string culture, IPublishedContent[] documents, params string[] names) +// { +// Assert.AreEqual(names.Length, documents.Length); +// for (var i = 0; i < names.Length; i++) +// { +// Assert.AreEqual(names[i], documents[i].Name(VariationContextAccessor, culture)); +// } +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceContentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceContentTests.cs index 570cd19566..e80ac3929d 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceContentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceContentTests.cs @@ -1,206 +1,207 @@ -using System.Collections.Generic; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Services.Changes; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Builders.Extensions; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; - -[TestFixture] -public class PublishedSnapshotServiceContentTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - _propertyType = - new PropertyType(TestHelper.ShortStringHelper, "Umbraco.Void.Editor", ValueStorageType.Nvarchar) - { - Alias = "prop", - DataTypeId = 3, - Variations = ContentVariation.Culture, - }; - _contentType = - new ContentType(TestHelper.ShortStringHelper, -1) - { - Id = 2, - Alias = "alias-ct", - Variations = ContentVariation.Culture, - }; - _contentType.AddPropertyType(_propertyType); - - var contentTypes = new[] { _contentType }; - - InitializedCache(new[] { CreateKit() }, contentTypes); - } - - private ContentType _contentType; - private PropertyType _propertyType; - - private ContentNodeKit CreateKit() - { - var draftData = new ContentDataBuilder() - .WithName("It Works2!") - .WithPublished(false) - .WithProperties(new Dictionary - { - ["prop"] = new[] - { - new PropertyData { Culture = string.Empty, Segment = string.Empty, Value = "val2" }, - new PropertyData { Culture = "fr-FR", Segment = string.Empty, Value = "val-fr2" }, - new PropertyData { Culture = "en-UK", Segment = string.Empty, Value = "val-uk2" }, - new PropertyData { Culture = "dk-DA", Segment = string.Empty, Value = "val-da2" }, - new PropertyData { Culture = "de-DE", Segment = string.Empty, Value = "val-de2" }, - }, - }) - .WithCultureInfos(new Dictionary - { - // draft data = everything, and IsDraft indicates what's edited - ["fr-FR"] = new() { Name = "name-fr2", IsDraft = true, Date = new DateTime(2018, 01, 03, 01, 00, 00) }, - ["en-UK"] = new() { Name = "name-uk2", IsDraft = true, Date = new DateTime(2018, 01, 04, 01, 00, 00) }, - ["dk-DA"] = new() { Name = "name-da2", IsDraft = true, Date = new DateTime(2018, 01, 05, 01, 00, 00) }, - ["de-DE"] = new() { Name = "name-de1", IsDraft = false, Date = new DateTime(2018, 01, 02, 01, 00, 00) }, - }) - .Build(); - - var publishedData = new ContentDataBuilder() - .WithName("It Works1!") - .WithPublished(true) - .WithProperties(new Dictionary - { - ["prop"] = new[] - { - new PropertyData { Culture = string.Empty, Segment = string.Empty, Value = "val1" }, - new PropertyData { Culture = "fr-FR", Segment = string.Empty, Value = "val-fr1" }, - new PropertyData { Culture = "en-UK", Segment = string.Empty, Value = "val-uk1" }, - }, - }) - .WithCultureInfos(new Dictionary - { - // published data = only what's actually published, and IsDraft has to be false - ["fr-FR"] = new() { Name = "name-fr1", IsDraft = false, Date = new DateTime(2018, 01, 01, 01, 00, 00) }, - ["en-UK"] = new() { Name = "name-uk1", IsDraft = false, Date = new DateTime(2018, 01, 02, 01, 00, 00) }, - ["de-DE"] = new() { Name = "name-de1", IsDraft = false, Date = new DateTime(2018, 01, 02, 01, 00, 00) }, - }) - .Build(); - - var kit = ContentNodeKitBuilder.CreateWithContent( - 2, - 1, - "-1,1", - 0, - draftData: draftData, - publishedData: publishedData); - - return kit; - } - - [Test] - public void Verifies_Variant_Data() - { - // this test implements a full standalone NuCache (based upon a test IDataSource, does not - // use any local db files, does not rely on any database) - and tests variations - - // get a snapshot, get a published content - var snapshot = GetPublishedSnapshot(); - var publishedContent = snapshot.Content.GetById(1); - - Assert.IsNotNull(publishedContent); - Assert.AreEqual("val1", publishedContent.Value(Mock.Of(), "prop")); - Assert.AreEqual("val-fr1", publishedContent.Value(Mock.Of(), "prop", "fr-FR")); - Assert.AreEqual("val-uk1", publishedContent.Value(Mock.Of(), "prop", "en-UK")); - - Assert.AreEqual(publishedContent.Name(VariationContextAccessor), string.Empty); // no invariant name for varying content - Assert.AreEqual("name-fr1", publishedContent.Name(VariationContextAccessor, "fr-FR")); - Assert.AreEqual("name-uk1", publishedContent.Name(VariationContextAccessor, "en-UK")); - - var draftContent = snapshot.Content.GetById(true, 1); - Assert.AreEqual("val2", draftContent.Value(Mock.Of(), "prop")); - Assert.AreEqual("val-fr2", draftContent.Value(Mock.Of(), "prop", "fr-FR")); - Assert.AreEqual("val-uk2", draftContent.Value(Mock.Of(), "prop", "en-UK")); - - Assert.AreEqual(draftContent.Name(VariationContextAccessor), string.Empty); // no invariant name for varying content - Assert.AreEqual("name-fr2", draftContent.Name(VariationContextAccessor, "fr-FR")); - Assert.AreEqual("name-uk2", draftContent.Name(VariationContextAccessor, "en-UK")); - - // now french is default - VariationContextAccessor.VariationContext = new VariationContext("fr-FR"); - Assert.AreEqual("val-fr1", publishedContent.Value(Mock.Of(), "prop")); - Assert.AreEqual("name-fr1", publishedContent.Name(VariationContextAccessor)); - Assert.AreEqual(new DateTime(2018, 01, 01, 01, 00, 00), publishedContent.CultureDate(VariationContextAccessor)); - - // now uk is default - VariationContextAccessor.VariationContext = new VariationContext("en-UK"); - Assert.AreEqual("val-uk1", publishedContent.Value(Mock.Of(), "prop")); - Assert.AreEqual("name-uk1", publishedContent.Name(VariationContextAccessor)); - Assert.AreEqual(new DateTime(2018, 01, 02, 01, 00, 00), publishedContent.CultureDate(VariationContextAccessor)); - - // invariant needs to be retrieved explicitly, when it's not default - Assert.AreEqual("val1", publishedContent.Value(Mock.Of(), "prop", string.Empty)); - - // but, - // if the content type / property type does not vary, then it's all invariant again - // modify the content type and property type, notify the snapshot service - _contentType.Variations = ContentVariation.Nothing; - _propertyType.Variations = ContentVariation.Nothing; - SnapshotService.Notify(new[] - { - new ContentTypeCacheRefresher.JsonPayload("IContentType", publishedContent.ContentType.Id, ContentTypeChangeTypes.RefreshMain), - }); - - // get a new snapshot (nothing changed in the old one), get the published content again - var anotherSnapshot = SnapshotService.CreatePublishedSnapshot(null); - var againContent = anotherSnapshot.Content.GetById(1); - - Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.Variations); - Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.GetPropertyType("prop").Variations); - - // now, "no culture" means "invariant" - Assert.AreEqual("It Works1!", againContent.Name(VariationContextAccessor)); - Assert.AreEqual("val1", againContent.Value(Mock.Of(), "prop")); - } - - [Test] - public void Verifies_Published_And_Draft_Content() - { - // get the published published content - var snapshot = GetPublishedSnapshot(); - var c1 = snapshot.Content.GetById(1); - - // published content = nothing is draft here - Assert.IsFalse(c1.IsDraft("fr-FR")); - Assert.IsFalse(c1.IsDraft("en-UK")); - Assert.IsFalse(c1.IsDraft("dk-DA")); - Assert.IsFalse(c1.IsDraft("de-DE")); - - // and only those with published name, are published - Assert.IsTrue(c1.IsPublished("fr-FR")); - Assert.IsTrue(c1.IsPublished("en-UK")); - Assert.IsFalse(c1.IsDraft("dk-DA")); - Assert.IsTrue(c1.IsPublished("de-DE")); - - // get the draft published content - var c2 = snapshot.Content.GetById(true, 1); - - // draft content = we have drafts - Assert.IsTrue(c2.IsDraft("fr-FR")); - Assert.IsTrue(c2.IsDraft("en-UK")); - Assert.IsTrue(c2.IsDraft("dk-DA")); - Assert.IsFalse(c2.IsDraft("de-DE")); // except for the one that does not - - // and only those with published name, are published - Assert.IsTrue(c2.IsPublished("fr-FR")); - Assert.IsTrue(c2.IsPublished("en-UK")); - Assert.IsFalse(c2.IsPublished("dk-DA")); - Assert.IsTrue(c2.IsPublished("de-DE")); - } -} +// using System.Collections.Generic; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Cache; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Models.PublishedContent; +// using Umbraco.Cms.Core.Services.Changes; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +// using Umbraco.Cms.Tests.Common.Builders; +// using Umbraco.Cms.Tests.Common.Builders.Extensions; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// using Umbraco.Extensions; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class PublishedSnapshotServiceContentTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// _propertyType = +// new PropertyType(TestHelper.ShortStringHelper, "Umbraco.Void.Editor", ValueStorageType.Nvarchar) +// { +// Alias = "prop", +// DataTypeId = 3, +// Variations = ContentVariation.Culture, +// }; +// _contentType = +// new ContentType(TestHelper.ShortStringHelper, -1) +// { +// Id = 2, +// Alias = "alias-ct", +// Variations = ContentVariation.Culture, +// }; +// _contentType.AddPropertyType(_propertyType); +// +// var contentTypes = new[] { _contentType }; +// +// InitializedCache(new[] { CreateKit() }, contentTypes); +// } +// +// private ContentType _contentType; +// private PropertyType _propertyType; +// +// private ContentNodeKit CreateKit() +// { +// var draftData = new ContentDataBuilder() +// .WithName("It Works2!") +// .WithPublished(false) +// .WithProperties(new Dictionary +// { +// ["prop"] = new[] +// { +// new PropertyData { Culture = string.Empty, Segment = string.Empty, Value = "val2" }, +// new PropertyData { Culture = "fr-FR", Segment = string.Empty, Value = "val-fr2" }, +// new PropertyData { Culture = "en-UK", Segment = string.Empty, Value = "val-uk2" }, +// new PropertyData { Culture = "dk-DA", Segment = string.Empty, Value = "val-da2" }, +// new PropertyData { Culture = "de-DE", Segment = string.Empty, Value = "val-de2" }, +// }, +// }) +// .WithCultureInfos(new Dictionary +// { +// // draft data = everything, and IsDraft indicates what's edited +// ["fr-FR"] = new() { Name = "name-fr2", IsDraft = true, Date = new DateTime(2018, 01, 03, 01, 00, 00) }, +// ["en-UK"] = new() { Name = "name-uk2", IsDraft = true, Date = new DateTime(2018, 01, 04, 01, 00, 00) }, +// ["dk-DA"] = new() { Name = "name-da2", IsDraft = true, Date = new DateTime(2018, 01, 05, 01, 00, 00) }, +// ["de-DE"] = new() { Name = "name-de1", IsDraft = false, Date = new DateTime(2018, 01, 02, 01, 00, 00) }, +// }) +// .Build(); +// +// var publishedData = new ContentDataBuilder() +// .WithName("It Works1!") +// .WithPublished(true) +// .WithProperties(new Dictionary +// { +// ["prop"] = new[] +// { +// new PropertyData { Culture = string.Empty, Segment = string.Empty, Value = "val1" }, +// new PropertyData { Culture = "fr-FR", Segment = string.Empty, Value = "val-fr1" }, +// new PropertyData { Culture = "en-UK", Segment = string.Empty, Value = "val-uk1" }, +// }, +// }) +// .WithCultureInfos(new Dictionary +// { +// // published data = only what's actually published, and IsDraft has to be false +// ["fr-FR"] = new() { Name = "name-fr1", IsDraft = false, Date = new DateTime(2018, 01, 01, 01, 00, 00) }, +// ["en-UK"] = new() { Name = "name-uk1", IsDraft = false, Date = new DateTime(2018, 01, 02, 01, 00, 00) }, +// ["de-DE"] = new() { Name = "name-de1", IsDraft = false, Date = new DateTime(2018, 01, 02, 01, 00, 00) }, +// }) +// .Build(); +// +// var kit = ContentNodeKitBuilder.CreateWithContent( +// 2, +// 1, +// "-1,1", +// 0, +// draftData: draftData, +// publishedData: publishedData); +// +// return kit; +// } +// +// [Test] +// public void Verifies_Variant_Data() +// { +// // this test implements a full standalone NuCache (based upon a test IDataSource, does not +// // use any local db files, does not rely on any database) - and tests variations +// +// // get a snapshot, get a published content +// var snapshot = GetPublishedSnapshot(); +// var publishedContent = snapshot.Content.GetById(1); +// +// Assert.IsNotNull(publishedContent); +// Assert.AreEqual("val1", publishedContent.Value(Mock.Of(), "prop")); +// Assert.AreEqual("val-fr1", publishedContent.Value(Mock.Of(), "prop", "fr-FR")); +// Assert.AreEqual("val-uk1", publishedContent.Value(Mock.Of(), "prop", "en-UK")); +// +// Assert.AreEqual(publishedContent.Name(VariationContextAccessor), string.Empty); // no invariant name for varying content +// Assert.AreEqual("name-fr1", publishedContent.Name(VariationContextAccessor, "fr-FR")); +// Assert.AreEqual("name-uk1", publishedContent.Name(VariationContextAccessor, "en-UK")); +// +// var draftContent = snapshot.Content.GetById(true, 1); +// Assert.AreEqual("val2", draftContent.Value(Mock.Of(), "prop")); +// Assert.AreEqual("val-fr2", draftContent.Value(Mock.Of(), "prop", "fr-FR")); +// Assert.AreEqual("val-uk2", draftContent.Value(Mock.Of(), "prop", "en-UK")); +// +// Assert.AreEqual(draftContent.Name(VariationContextAccessor), string.Empty); // no invariant name for varying content +// Assert.AreEqual("name-fr2", draftContent.Name(VariationContextAccessor, "fr-FR")); +// Assert.AreEqual("name-uk2", draftContent.Name(VariationContextAccessor, "en-UK")); +// +// // now french is default +// VariationContextAccessor.VariationContext = new VariationContext("fr-FR"); +// Assert.AreEqual("val-fr1", publishedContent.Value(Mock.Of(), "prop")); +// Assert.AreEqual("name-fr1", publishedContent.Name(VariationContextAccessor)); +// Assert.AreEqual(new DateTime(2018, 01, 01, 01, 00, 00), publishedContent.CultureDate(VariationContextAccessor)); +// +// // now uk is default +// VariationContextAccessor.VariationContext = new VariationContext("en-UK"); +// Assert.AreEqual("val-uk1", publishedContent.Value(Mock.Of(), "prop")); +// Assert.AreEqual("name-uk1", publishedContent.Name(VariationContextAccessor)); +// Assert.AreEqual(new DateTime(2018, 01, 02, 01, 00, 00), publishedContent.CultureDate(VariationContextAccessor)); +// +// // invariant needs to be retrieved explicitly, when it's not default +// Assert.AreEqual("val1", publishedContent.Value(Mock.Of(), "prop", string.Empty)); +// +// // but, +// // if the content type / property type does not vary, then it's all invariant again +// // modify the content type and property type, notify the snapshot service +// _contentType.Variations = ContentVariation.Nothing; +// _propertyType.Variations = ContentVariation.Nothing; +// SnapshotService.Notify(new[] +// { +// new ContentTypeCacheRefresher.JsonPayload("IContentType", publishedContent.ContentType.Id, ContentTypeChangeTypes.RefreshMain), +// }); +// +// // get a new snapshot (nothing changed in the old one), get the published content again +// var anotherSnapshot = SnapshotService.CreatePublishedSnapshot(null); +// var againContent = anotherSnapshot.Content.GetById(1); +// +// Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.Variations); +// Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.GetPropertyType("prop").Variations); +// +// // now, "no culture" means "invariant" +// Assert.AreEqual("It Works1!", againContent.Name(VariationContextAccessor)); +// Assert.AreEqual("val1", againContent.Value(Mock.Of(), "prop")); +// } +// +// [Test] +// public void Verifies_Published_And_Draft_Content() +// { +// // get the published published content +// var snapshot = GetPublishedSnapshot(); +// var c1 = snapshot.Content.GetById(1); +// +// // published content = nothing is draft here +// Assert.IsFalse(c1.IsDraft("fr-FR")); +// Assert.IsFalse(c1.IsDraft("en-UK")); +// Assert.IsFalse(c1.IsDraft("dk-DA")); +// Assert.IsFalse(c1.IsDraft("de-DE")); +// +// // and only those with published name, are published +// Assert.IsTrue(c1.IsPublished("fr-FR")); +// Assert.IsTrue(c1.IsPublished("en-UK")); +// Assert.IsFalse(c1.IsDraft("dk-DA")); +// Assert.IsTrue(c1.IsPublished("de-DE")); +// +// // get the draft published content +// var c2 = snapshot.Content.GetById(true, 1); +// +// // draft content = we have drafts +// Assert.IsTrue(c2.IsDraft("fr-FR")); +// Assert.IsTrue(c2.IsDraft("en-UK")); +// Assert.IsTrue(c2.IsDraft("dk-DA")); +// Assert.IsFalse(c2.IsDraft("de-DE")); // except for the one that does not +// +// // and only those with published name, are published +// Assert.IsTrue(c2.IsPublished("fr-FR")); +// Assert.IsTrue(c2.IsPublished("en-UK")); +// Assert.IsFalse(c2.IsPublished("dk-DA")); +// Assert.IsTrue(c2.IsPublished("de-DE")); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/RootNodeTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/RootNodeTests.cs index 666eadff08..dc0baa6b18 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/RootNodeTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/RootNodeTests.cs @@ -1,48 +1,49 @@ -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; - -[TestFixture] -public class RootNodeTests : PublishedSnapshotServiceTestBase -{ - [SetUp] - public override void Setup() - { - base.Setup(); - - var xml = PublishedContentXml.TestWithDatabaseXml(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - } - - [Test] - public void PublishedContentHasNoRootNode() - { - var snapshot = GetPublishedSnapshot(); - - // there is no content node with ID -1 - var content = snapshot.Content.GetById(-1); - Assert.IsNull(content); - - // content at root has null parent - content = snapshot.Content.GetById(1046); - Assert.IsNotNull(content); - Assert.AreEqual(1, content.Level); - Assert.IsNull(content.Parent); - - // non-existing content is null - content = snapshot.Content.GetById(666); - Assert.IsNull(content); - } -} +// using System.Collections.Generic; +// using System.Linq; +// using NUnit.Framework; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Published; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class RootNodeTests : PublishedSnapshotServiceTestBase +// { +// [SetUp] +// public override void Setup() +// { +// base.Setup(); +// +// var xml = PublishedContentXml.TestWithDatabaseXml(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// } +// +// [Test] +// public void PublishedContentHasNoRootNode() +// { +// var snapshot = GetPublishedSnapshot(); +// +// // there is no content node with ID -1 +// var content = snapshot.Content.GetById(-1); +// Assert.IsNull(content); +// +// // content at root has null parent +// content = snapshot.Content.GetById(1046); +// Assert.IsNotNull(content); +// Assert.AreEqual(1, content.Level); +// Assert.IsNull(content.Parent); +// +// // non-existing content is null +// content = snapshot.Content.GetById(666); +// Assert.IsNull(content); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/UrlRoutesTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/UrlRoutesTests.cs index 9d4c8efc99..e0bb426b4f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/UrlRoutesTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/UrlRoutesTests.cs @@ -1,365 +1,366 @@ -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; - -// purpose: test the values returned by PublishedContentCache.GetRouteById -// and .GetByRoute (no caching at all, just routing nice URLs) including all -// the quirks due to hideTopLevelFromPath and backward compatibility. -public class UrlRoutesTests : PublishedSnapshotServiceTestBase -{ - private static string GetXmlContent(int templateId) - => @" - - -]> - - - - - - - - - - - - - - - - - - - - - - - -"; - - /* - * Just so it's documented somewhere, as of jan. 2017, routes obey the following pseudo-code: - -GetByRoute(route, hide = null): - -route is "[id]/[path]" - -hide = hide ?? global.hide - -root = id ? node(id) : document - -content = cached(route) ?? DetermineIdByRoute(route, hide) - -# route is "1234/path/to/content", finds "content" -# but if there is domain 5678 on "to", the *true* route of "content" is "5678/content" -# so although the route does match, we don't cache it -# there are not other reason not to cache it - -if content and no domain between root and content: - cache route (as trusted) - -return content - - -DetermineIdByRoute(route, hide): - -route is "[id]/[path]" - -try return NavigateRoute(id ?? 0, path, hide:hide) -return null - - -NavigateRoute(id, path, hide): - -if path: - if id: - start = node(id) - else: - start = document - - # 'navigate ... from ...' uses lowest sortOrder in case of collision - - if hide and ![id]: - # if hiding, then for "/foo" we want to look for "/[any]/foo" - for each child of start: - try return navigate path from child - - # but if it fails, we also want to try "/foo" - # fail now if more than one part eg "/foo/bar" - if path is "/[any]/...": - fail - - try return navigate path from start - -else: - if id: - return node(id) - else: - return root node with lowest sortOrder - - -GetRouteById(id): - - -route = cached(id) -if route: - return route - -# never cache the route, it may be colliding - -route = DetermineRouteById(id) -if route: - cache route (as not trusted) - -return route - - - -DetermineRouteById(id): - - -node = node(id) - -walk up from node to domain or root, assemble parts = URL segments - -if !domain and global.hide: - if id.parent: - # got /top/[path]content, can remove /top - remove top part - else: - # got /content, should remove only if it is the - # node with lowest sort order - root = root node with lowest sortOrder - if root == node: - remove top part - -compose path from parts -route = assemble "[domain.id]/[path]" -return route - - */ - - /* - * The Xml structure for the following tests is: - * - * root - * A 1000 - * B 1001 - * C 1002 - * D 1003 - * X 2000 - * Y 2001 - * Z 2002 - * A 2003 - * B 2004 - * C 2005 - * E 2006 - * - */ - - [TestCase(1000, false, "/a")] - [TestCase(1001, false, "/a/b")] - [TestCase(1002, false, "/a/b/c")] - [TestCase(1003, false, "/a/b/c/d")] - [TestCase(2000, false, "/x")] - [TestCase(2001, false, "/x/y")] - [TestCase(2002, false, "/x/y/z")] - [TestCase(2003, false, "/x/a")] - [TestCase(2004, false, "/x/b")] - [TestCase(2005, false, "/x/b/c")] - [TestCase(2006, false, "/x/b/e")] - public void GetRouteByIdNoHide(int id, bool hide, string expected) - { - GlobalSettings.HideTopLevelNodeFromPath = hide; - - var xml = GetXmlContent(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - - var cache = GetPublishedSnapshot().Content; - - var route = cache.GetRouteById(false, id); - Assert.AreEqual(expected, route); - } - - [TestCase(1000, true, "/")] - [TestCase(1001, true, "/b")] - [TestCase(1002, true, "/b/c")] - [TestCase(1003, true, "/b/c/d")] - [TestCase(2000, true, "/x")] - [TestCase(2001, true, "/y")] - [TestCase(2002, true, "/y/z")] - [TestCase(2003, true, "/a")] - [TestCase(2004, true, "/b")] // collision! - [TestCase(2005, true, "/b/c")] // collision! - [TestCase(2006, true, "/b/e")] // risky! - public void GetRouteByIdHide(int id, bool hide, string expected) - { - GlobalSettings.HideTopLevelNodeFromPath = hide; - - var xml = GetXmlContent(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - - var cache = GetPublishedSnapshot().Content; - - var route = cache.GetRouteById(false, id); - Assert.AreEqual(expected, route); - } - - [Test] - public void GetRouteByIdCache() - { - GlobalSettings.HideTopLevelNodeFromPath = false; - - var xml = GetXmlContent(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - - var cache = GetPublishedSnapshot().Content; - - var route = cache.GetRouteById(false, 1000); - Assert.AreEqual("/a", route); - } - - [TestCase("/", false, 1000)] - [TestCase("/a", false, 1000)] // yes! - [TestCase("/a/b", false, 1001)] - [TestCase("/a/b/c", false, 1002)] - [TestCase("/a/b/c/d", false, 1003)] - [TestCase("/x", false, 2000)] - public void GetByRouteNoHide(string route, bool hide, int expected) - { - GlobalSettings.HideTopLevelNodeFromPath = hide; - - var xml = GetXmlContent(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - - var cache = GetPublishedSnapshot().Content; - - const bool preview = false; // make sure we don't cache - but HOW? should be some sort of switch?! - var content = cache.GetByRoute(preview, route); - if (expected < 0) - { - Assert.IsNull(content); - } - else - { - Assert.IsNotNull(content); - Assert.AreEqual(expected, content.Id); - } - } - - [TestCase("/", true, 1000)] - [TestCase("/a", true, 2003)] - [TestCase("/a/b", true, -1)] - [TestCase("/x", true, 2000)] // oops! - [TestCase("/x/y", true, -1)] // yes! - [TestCase("/y", true, 2001)] - [TestCase("/y/z", true, 2002)] - [TestCase("/b", true, 1001)] // (hence the 2004 collision) - [TestCase("/b/c", true, 1002)] // (hence the 2005 collision) - public void GetByRouteHide(string route, bool hide, int expected) - { - GlobalSettings.HideTopLevelNodeFromPath = hide; - - var xml = GetXmlContent(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - - var cache = GetPublishedSnapshot().Content; - - const bool preview = false; // make sure we don't cache - but HOW? should be some sort of switch?! - var content = cache.GetByRoute(preview, route); - if (expected < 0) - { - Assert.IsNull(content); - } - else - { - Assert.IsNotNull(content); - Assert.AreEqual(expected, content.Id); - } - } - - [Test] - public void GetByRouteCache() - { - GlobalSettings.HideTopLevelNodeFromPath = false; - - var xml = GetXmlContent(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); - - var cache = GetPublishedSnapshot().Content; - - var content = cache.GetByRoute(false, "/a/b/c"); - Assert.IsNotNull(content); - Assert.AreEqual(1002, content.Id); - } -} +// using System.Collections.Generic; +// using System.Linq; +// using NUnit.Framework; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// using Umbraco.Cms.Tests.Common.Published; +// using Umbraco.Cms.Tests.UnitTests.TestHelpers; +// +// FIXME: Reintroduce if relevant +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; +// +// // purpose: test the values returned by PublishedContentCache.GetRouteById +// // and .GetByRoute (no caching at all, just routing nice URLs) including all +// // the quirks due to hideTopLevelFromPath and backward compatibility. +// public class UrlRoutesTests : PublishedSnapshotServiceTestBase +// { +// private static string GetXmlContent(int templateId) +// => @" +// +// +// ]> +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// "; +// +// /* +// * Just so it's documented somewhere, as of jan. 2017, routes obey the following pseudo-code: +// +// GetByRoute(route, hide = null): +// +// route is "[id]/[path]" +// +// hide = hide ?? global.hide +// +// root = id ? node(id) : document +// +// content = cached(route) ?? DetermineIdByRoute(route, hide) +// +// # route is "1234/path/to/content", finds "content" +// # but if there is domain 5678 on "to", the *true* route of "content" is "5678/content" +// # so although the route does match, we don't cache it +// # there are not other reason not to cache it +// +// if content and no domain between root and content: +// cache route (as trusted) +// +// return content +// +// +// DetermineIdByRoute(route, hide): +// +// route is "[id]/[path]" +// +// try return NavigateRoute(id ?? 0, path, hide:hide) +// return null +// +// +// NavigateRoute(id, path, hide): +// +// if path: +// if id: +// start = node(id) +// else: +// start = document +// +// # 'navigate ... from ...' uses lowest sortOrder in case of collision +// +// if hide and ![id]: +// # if hiding, then for "/foo" we want to look for "/[any]/foo" +// for each child of start: +// try return navigate path from child +// +// # but if it fails, we also want to try "/foo" +// # fail now if more than one part eg "/foo/bar" +// if path is "/[any]/...": +// fail +// +// try return navigate path from start +// +// else: +// if id: +// return node(id) +// else: +// return root node with lowest sortOrder +// +// +// GetRouteById(id): +// +// +// route = cached(id) +// if route: +// return route +// +// # never cache the route, it may be colliding +// +// route = DetermineRouteById(id) +// if route: +// cache route (as not trusted) +// +// return route +// +// +// +// DetermineRouteById(id): +// +// +// node = node(id) +// +// walk up from node to domain or root, assemble parts = URL segments +// +// if !domain and global.hide: +// if id.parent: +// # got /top/[path]content, can remove /top +// remove top part +// else: +// # got /content, should remove only if it is the +// # node with lowest sort order +// root = root node with lowest sortOrder +// if root == node: +// remove top part +// +// compose path from parts +// route = assemble "[domain.id]/[path]" +// return route +// +// */ +// +// /* +// * The Xml structure for the following tests is: +// * +// * root +// * A 1000 +// * B 1001 +// * C 1002 +// * D 1003 +// * X 2000 +// * Y 2001 +// * Z 2002 +// * A 2003 +// * B 2004 +// * C 2005 +// * E 2006 +// * +// */ +// +// [TestCase(1000, false, "/a")] +// [TestCase(1001, false, "/a/b")] +// [TestCase(1002, false, "/a/b/c")] +// [TestCase(1003, false, "/a/b/c/d")] +// [TestCase(2000, false, "/x")] +// [TestCase(2001, false, "/x/y")] +// [TestCase(2002, false, "/x/y/z")] +// [TestCase(2003, false, "/x/a")] +// [TestCase(2004, false, "/x/b")] +// [TestCase(2005, false, "/x/b/c")] +// [TestCase(2006, false, "/x/b/e")] +// public void GetRouteByIdNoHide(int id, bool hide, string expected) +// { +// GlobalSettings.HideTopLevelNodeFromPath = hide; +// +// var xml = GetXmlContent(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// var cache = GetPublishedSnapshot().Content; +// +// var route = cache.GetRouteById(false, id); +// Assert.AreEqual(expected, route); +// } +// +// [TestCase(1000, true, "/")] +// [TestCase(1001, true, "/b")] +// [TestCase(1002, true, "/b/c")] +// [TestCase(1003, true, "/b/c/d")] +// [TestCase(2000, true, "/x")] +// [TestCase(2001, true, "/y")] +// [TestCase(2002, true, "/y/z")] +// [TestCase(2003, true, "/a")] +// [TestCase(2004, true, "/b")] // collision! +// [TestCase(2005, true, "/b/c")] // collision! +// [TestCase(2006, true, "/b/e")] // risky! +// public void GetRouteByIdHide(int id, bool hide, string expected) +// { +// GlobalSettings.HideTopLevelNodeFromPath = hide; +// +// var xml = GetXmlContent(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// var cache = GetPublishedSnapshot().Content; +// +// var route = cache.GetRouteById(false, id); +// Assert.AreEqual(expected, route); +// } +// +// [Test] +// public void GetRouteByIdCache() +// { +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var xml = GetXmlContent(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// var cache = GetPublishedSnapshot().Content; +// +// var route = cache.GetRouteById(false, 1000); +// Assert.AreEqual("/a", route); +// } +// +// [TestCase("/", false, 1000)] +// [TestCase("/a", false, 1000)] // yes! +// [TestCase("/a/b", false, 1001)] +// [TestCase("/a/b/c", false, 1002)] +// [TestCase("/a/b/c/d", false, 1003)] +// [TestCase("/x", false, 2000)] +// public void GetByRouteNoHide(string route, bool hide, int expected) +// { +// GlobalSettings.HideTopLevelNodeFromPath = hide; +// +// var xml = GetXmlContent(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// var cache = GetPublishedSnapshot().Content; +// +// const bool preview = false; // make sure we don't cache - but HOW? should be some sort of switch?! +// var content = cache.GetByRoute(preview, route); +// if (expected < 0) +// { +// Assert.IsNull(content); +// } +// else +// { +// Assert.IsNotNull(content); +// Assert.AreEqual(expected, content.Id); +// } +// } +// +// [TestCase("/", true, 1000)] +// [TestCase("/a", true, 2003)] +// [TestCase("/a/b", true, -1)] +// [TestCase("/x", true, 2000)] // oops! +// [TestCase("/x/y", true, -1)] // yes! +// [TestCase("/y", true, 2001)] +// [TestCase("/y/z", true, 2002)] +// [TestCase("/b", true, 1001)] // (hence the 2004 collision) +// [TestCase("/b/c", true, 1002)] // (hence the 2005 collision) +// public void GetByRouteHide(string route, bool hide, int expected) +// { +// GlobalSettings.HideTopLevelNodeFromPath = hide; +// +// var xml = GetXmlContent(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// var cache = GetPublishedSnapshot().Content; +// +// const bool preview = false; // make sure we don't cache - but HOW? should be some sort of switch?! +// var content = cache.GetByRoute(preview, route); +// if (expected < 0) +// { +// Assert.IsNull(content); +// } +// else +// { +// Assert.IsNotNull(content); +// Assert.AreEqual(expected, content.Id); +// } +// } +// +// [Test] +// public void GetByRouteCache() +// { +// GlobalSettings.HideTopLevelNodeFromPath = false; +// +// var xml = GetXmlContent(1234); +// +// IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( +// xml, +// TestHelper.ShortStringHelper, +// out var contentTypes, +// out var dataTypes).ToList(); +// +// InitializedCache(kits, contentTypes, dataTypes); +// +// var cache = GetPublishedSnapshot().Content; +// +// var content = cache.GetByRoute(false, "/a/b/c"); +// Assert.IsNotNull(content); +// Assert.AreEqual(1002, content.Id); +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs index b1e1702515..f4290eb6f5 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs @@ -55,9 +55,9 @@ public class MemberManagerTests new UmbracoMapper(new MapDefinitionCollection(() => mapDefinitions), scopeProvider, NullLogger.Instance), scopeProvider, new IdentityErrorDescriber(), - Mock.Of(), Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); _mockIdentityOptions = new Mock>(); var idOptions = new IdentityOptions { Lockout = { AllowedForNewUsers = false } }; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs index 3451b903f8..17f41c047c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs @@ -36,9 +36,9 @@ public class MemberUserStoreTests new UmbracoMapper(new MapDefinitionCollection(() => new List()), mockScopeProvider, NullLogger.Instance), mockScopeProvider, new IdentityErrorDescriber(), - Mock.Of(), Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); } [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs index 3327e6f27d..6f3d2a7356 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs @@ -83,13 +83,13 @@ namespace Umbraco.Cms.Web.Common.PublishedModels [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] - public new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor) - => PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias); + public new static IPublishedContentType GetModelContentType(IPublishedContentTypeCache contentTypeCache) + => PublishedModelUtility.GetModelContentType(contentTypeCache, ModelItemType, ModelTypeAlias); [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] - public static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector) - => PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector); + public static IPublishedPropertyType GetModelPropertyType(IPublishedContentTypeCache contentTypeCache, Expression> selector) + => PublishedModelUtility.GetModelPropertyType(GetModelContentType(contentTypeCache), selector); #pragma warning restore 0109 private IPublishedValueFallback _publishedValueFallback; @@ -200,13 +200,13 @@ namespace Umbraco.Cms.Web.Common.PublishedModels [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] - public new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor) - => PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias); + public new static IPublishedContentType GetModelContentType(IPublishedContentTypeCache contentTypeCache) + => PublishedModelUtility.GetModelContentType(contentTypeCache, ModelItemType, ModelTypeAlias); [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] - public static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector) - => PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector); + public static IPublishedPropertyType GetModelPropertyType(IPublishedContentTypeCache contentTypeCache, Expression> selector) + => PublishedModelUtility.GetModelPropertyType(GetModelContentType(contentTypeCache), selector); #pragma warning restore 0109 private IPublishedValueFallback _publishedValueFallback; @@ -367,13 +367,13 @@ namespace Umbraco.Cms.Web.Common.PublishedModels [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] - public new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor) - => PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias); + public new static IPublishedContentType GetModelContentType(IPublishedContentTypeCache contentTypeCache) + => PublishedModelUtility.GetModelContentType(contentTypeCache, ModelItemType, ModelTypeAlias); [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] - public static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector) - => PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector); + public static IPublishedPropertyType GetModelPropertyType(IPublishedContentTypeCache contentTypeCache, Expression> selector) + => PublishedModelUtility.GetModelPropertyType(GetModelContentType(contentTypeCache), selector); #pragma warning restore 0109 private IPublishedValueFallback _publishedValueFallback; @@ -440,13 +440,13 @@ namespace Umbraco.Cms.Web.Common.PublishedModels [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] - public new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor) - => PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias); + public new static IPublishedContentType GetModelContentType(IPublishedContentTypeCache contentTypeCache) + => PublishedModelUtility.GetModelContentType(contentTypeCache, ModelItemType, ModelTypeAlias); [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] - public static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector) - => PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector); + public static IPublishedPropertyType GetModelPropertyType(IPublishedContentTypeCache contentTypeCache, Expression> selector) + => PublishedModelUtility.GetModelPropertyType(GetModelContentType(contentTypeCache), selector); #pragma warning restore 0109 private IPublishedValueFallback _publishedValueFallback; @@ -556,13 +556,13 @@ namespace Umbraco.Cms.Web.Common.PublishedModels [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] - public new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor) - => PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias); + public new static IPublishedContentType GetModelContentType(IPublishedContentTypeCache contentTypeCache) + => PublishedModelUtility.GetModelContentType(contentTypeCache, ModelItemType, ModelTypeAlias); [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] - public static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector) - => PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector); + public static IPublishedPropertyType GetModelPropertyType(IPublishedContentTypeCache contentTypeCache, Expression> selector) + => PublishedModelUtility.GetModelPropertyType(GetModelContentType(contentTypeCache), selector); #pragma warning restore 0109 private IPublishedValueFallback _publishedValueFallback; @@ -649,13 +649,13 @@ namespace Umbraco.Cms.Web.Common.PublishedModels [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] - public new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor) - => PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias); + public new static IPublishedContentType GetModelContentType(IPublishedContentTypeCache contentTypeCache) + => PublishedModelUtility.GetModelContentType(contentTypeCache, ModelItemType, ModelTypeAlias); [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] - public static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector) - => PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector); + public static IPublishedPropertyType GetModelPropertyType(IPublishedContentTypeCache contentTypeCache, Expression> selector) + => PublishedModelUtility.GetModelPropertyType(GetModelContentType(contentTypeCache), selector); #pragma warning restore 0109 private IPublishedValueFallback _publishedValueFallback; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.NuCache/SnapDictionaryTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.NuCache/SnapDictionaryTests.cs index c34765fa3a..686115a7a3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.NuCache/SnapDictionaryTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.NuCache/SnapDictionaryTests.cs @@ -1,1180 +1,1181 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System.Linq; -using System.Threading.Tasks; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Infrastructure.PublishedCache; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.PublishedCache.NuCache; - -[TestFixture] -public class SnapDictionaryTests -{ - [Test] - public void LiveGenUpdate() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - Assert.AreEqual(0, d.Test.GetValues(1).Length); - - // gen 1 - d.Set(1, "one"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - d.Clear(1); - Assert.AreEqual(0, d.Test.GetValues(1).Length); // gone - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - Assert.AreEqual(0, d.Test.FloorGen); - } - - [Test] - public void OtherGenUpdate() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - Assert.AreEqual(0, d.Test.GetValues(1).Length); - Assert.AreEqual(0, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - // gen 1 - d.Set(1, "one"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s = d.CreateSnapshot(); - Assert.AreEqual(1, s.Gen); - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - // gen 2 - d.Clear(1); - Assert.AreEqual(2, d.Test.GetValues(1).Length); // there - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - Assert.AreEqual(0, d.Test.FloorGen); - - GC.KeepAlive(s); - } - - [Test] - public void MissingReturnsNull() - { - var d = new SnapDictionary(); - var s = d.CreateSnapshot(); - - Assert.IsNull(s.Get(1)); - } - - [Test] - public void DeletedReturnsNull() - { - var d = new SnapDictionary(); - - // gen 1 - d.Set(1, "one"); - - var s1 = d.CreateSnapshot(); - Assert.AreEqual("one", s1.Get(1)); - - // gen 2 - d.Clear(1); - - var s2 = d.CreateSnapshot(); - Assert.IsNull(s2.Get(1)); - - Assert.AreEqual("one", s1.Get(1)); - } - - [Retry(5)] // TODO make this test non-flaky. - [Test] - public async Task CollectValues() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - // gen 1 - d.Set(1, "one"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - d.Set(1, "one"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - d.Set(1, "uno"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s1 = d.CreateSnapshot(); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - // gen 2 - Assert.AreEqual(1, d.Test.GetValues(1).Length); - d.Set(1, "one"); - Assert.AreEqual(2, d.Test.GetValues(1).Length); - d.Set(1, "uno"); - Assert.AreEqual(2, d.Test.GetValues(1).Length); - - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s2 = d.CreateSnapshot(); - - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - // gen 3 - Assert.AreEqual(2, d.Test.GetValues(1).Length); - d.Set(1, "one"); - Assert.AreEqual(3, d.Test.GetValues(1).Length); - d.Set(1, "uno"); - Assert.AreEqual(3, d.Test.GetValues(1).Length); - - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var tv = d.Test.GetValues(1); - Assert.AreEqual(3, tv[0].Gen); - Assert.AreEqual(2, tv[1].Gen); - Assert.AreEqual(1, tv[2].Gen); - - Assert.AreEqual(0, d.Test.FloorGen); - - // nothing to collect - await d.CollectAsync(); - GC.KeepAlive(s1); - GC.KeepAlive(s2); - Assert.AreEqual(0, d.Test.FloorGen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - Assert.AreEqual(2, d.SnapCount); - Assert.AreEqual(3, d.Test.GetValues(1).Length); - - // one snapshot to collect - s1 = null; - GC.Collect(); - GC.KeepAlive(s2); - await d.CollectAsync(); - Assert.AreEqual(1, d.Test.FloorGen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - Assert.AreEqual(1, d.SnapCount); - Assert.AreEqual(2, d.Test.GetValues(1).Length); - - // another snapshot to collect - s2 = null; - GC.Collect(); - await d.CollectAsync(); - Assert.AreEqual(2, d.Test.FloorGen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - Assert.AreEqual(0, d.SnapCount); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - } - - [Test] - public async Task ProperlyCollects() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - for (var i = 0; i < 32; i++) - { - d.Set(i, i.ToString()); - d.CreateSnapshot().Dispose(); - } - - Assert.AreEqual(32, d.GenCount); - Assert.AreEqual(0, d.SnapCount); // because we've disposed them - - await d.CollectAsync(); - Assert.AreEqual(32, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - Assert.AreEqual(0, d.GenCount); - Assert.AreEqual(0, d.SnapCount); - Assert.AreEqual(32, d.Count); - - for (var i = 0; i < 32; i++) - { - d.Set(i, null); - } - - d.CreateSnapshot().Dispose(); - - // because we haven't collected yet, but disposed nevertheless - Assert.AreEqual(1, d.GenCount); - Assert.AreEqual(0, d.SnapCount); - Assert.AreEqual(32, d.Count); - - // once we collect, they are all gone - // since noone is interested anymore - await d.CollectAsync(); - Assert.AreEqual(0, d.GenCount); - Assert.AreEqual(0, d.SnapCount); - Assert.AreEqual(0, d.Count); - } - - [Retry(5)] // TODO make this test non-flaky. - [Test] - public async Task CollectNulls() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - // gen 1 - d.Set(1, "one"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - d.Set(1, "one"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - d.Set(1, "uno"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s1 = d.CreateSnapshot(); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - // gen 2 - Assert.AreEqual(1, d.Test.GetValues(1).Length); - d.Set(1, "one"); - Assert.AreEqual(2, d.Test.GetValues(1).Length); - d.Set(1, "uno"); - Assert.AreEqual(2, d.Test.GetValues(1).Length); - - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s2 = d.CreateSnapshot(); - - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - // gen 3 - Assert.AreEqual(2, d.Test.GetValues(1).Length); - d.Set(1, "one"); - Assert.AreEqual(3, d.Test.GetValues(1).Length); - d.Set(1, "uno"); - Assert.AreEqual(3, d.Test.GetValues(1).Length); - d.Clear(1); - Assert.AreEqual(3, d.Test.GetValues(1).Length); - - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var tv = d.Test.GetValues(1); - Assert.AreEqual(3, tv[0].Gen); - Assert.AreEqual(2, tv[1].Gen); - Assert.AreEqual(1, tv[2].Gen); - - Assert.AreEqual(0, d.Test.FloorGen); - - // nothing to collect - await d.CollectAsync(); - GC.KeepAlive(s1); - GC.KeepAlive(s2); - Assert.AreEqual(0, d.Test.FloorGen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - Assert.AreEqual(2, d.SnapCount); - Assert.AreEqual(3, d.Test.GetValues(1).Length); - - // one snapshot to collect - s1 = null; - GC.Collect(); - GC.KeepAlive(s2); - await d.CollectAsync(); - Assert.AreEqual(1, d.Test.FloorGen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - Assert.AreEqual(1, d.SnapCount); - Assert.AreEqual(2, d.Test.GetValues(1).Length); - - // another snapshot to collect - s2 = null; - GC.Collect(); - await d.CollectAsync(); - Assert.AreEqual(2, d.Test.FloorGen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - Assert.AreEqual(0, d.SnapCount); - - // and everything is gone? - // no, cannot collect the live gen because we'd need to lock - Assert.AreEqual(1, d.Test.GetValues(1).Length); - - d.CreateSnapshot(); - GC.Collect(); - await d.CollectAsync(); - - // poof, gone - Assert.AreEqual(0, d.Test.GetValues(1).Length); - } - - [Test] - [Retry(5)] // TODO make this test non-flaky. - public async Task EventuallyCollectNulls() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - Assert.AreEqual(0, d.Test.GetValues(1).Length); - - // gen 1 - d.Set(1, "one"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - await d.CollectAsync(); - var tv = d.Test.GetValues(1); - Assert.AreEqual(1, tv.Length); - Assert.AreEqual(1, tv[0].Gen); - - var s = d.CreateSnapshot(); - Assert.AreEqual("one", s.Get(1)); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - Assert.AreEqual(1, d.Count); - Assert.AreEqual(1, d.SnapCount); - Assert.AreEqual(1, d.GenCount); - - // gen 2 - d.Clear(1); - tv = d.Test.GetValues(1); - Assert.AreEqual(2, tv.Length); - Assert.AreEqual(2, tv[0].Gen); - - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - Assert.AreEqual(1, d.Count); - Assert.AreEqual(1, d.SnapCount); - Assert.AreEqual(1, d.GenCount); - - // nothing to collect - await d.CollectAsync(); - GC.KeepAlive(s); - Assert.AreEqual(2, d.Test.GetValues(1).Length); - - Assert.AreEqual(1, d.Count); - Assert.AreEqual(1, d.SnapCount); - Assert.AreEqual(1, d.GenCount); - - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - // collect snapshot - // don't collect liveGen+ - s = null; // without being disposed - GC.Collect(); // should release the generation reference - await d.CollectAsync(); - - Assert.AreEqual(1, d.Test.GetValues(1).Length); // "one" value is gone - Assert.AreEqual(1, d.Count); // still have 1 item - Assert.AreEqual(0, d.SnapCount); // snapshot is gone - Assert.AreEqual(0, d.GenCount); // and generation has been dequeued - - // liveGen/nextGen - s = d.CreateSnapshot(); - s = null; - - // collect liveGen - GC.Collect(); - - Assert.IsTrue(d.Test.GenObjs.TryPeek(out var genObj)); - genObj = null; - - // in Release mode, it works, but in Debug mode, the weak reference is still alive - // and for some reason we need to do this to ensure it is collected -#if DEBUG - await d.CollectAsync(); - GC.Collect(); -#endif - - Assert.IsTrue(d.Test.GenObjs.TryPeek(out genObj)); - Assert.IsFalse(genObj.WeakGenRef.IsAlive); // snapshot is gone, along with its reference - - await d.CollectAsync(); - - Assert.AreEqual(0, d.Test.GetValues(1).Length); // null value is gone - Assert.AreEqual(0, d.Count); // item is gone - Assert.AreEqual(0, d.Test.GenObjs.Count); - Assert.AreEqual(0, d.SnapCount); // snapshot is gone - Assert.AreEqual(0, d.GenCount); // and generation has been dequeued - } - - [Test] - public async Task CollectDisposedSnapshots() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - // gen 1 - d.Set(1, "one"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s1 = d.CreateSnapshot(); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - // gen 2 - d.Set(1, "two"); - Assert.AreEqual(2, d.Test.GetValues(1).Length); - - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s2 = d.CreateSnapshot(); - - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - // gen 3 - d.Set(1, "three"); - Assert.AreEqual(3, d.Test.GetValues(1).Length); - - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s3 = d.CreateSnapshot(); - - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - Assert.AreEqual(3, d.SnapCount); - - s1.Dispose(); - await d.CollectAsync(); - Assert.AreEqual(2, d.SnapCount); - Assert.AreEqual(2, d.Test.GetValues(1).Length); - - s2.Dispose(); - await d.CollectAsync(); - Assert.AreEqual(1, d.SnapCount); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - - s3.Dispose(); - await d.CollectAsync(); - Assert.AreEqual(0, d.SnapCount); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - } - - [Retry(5)] // TODO make this test non-flaky. - [Test] - public async Task CollectGcSnapshots() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - // gen 1 - d.Set(1, "one"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s1 = d.CreateSnapshot(); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - // gen 2 - d.Set(1, "two"); - Assert.AreEqual(2, d.Test.GetValues(1).Length); - - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s2 = d.CreateSnapshot(); - - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - // gen 3 - d.Set(1, "three"); - Assert.AreEqual(3, d.Test.GetValues(1).Length); - - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s3 = d.CreateSnapshot(); - - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - - Assert.AreEqual(3, d.SnapCount); - - s1 = s2 = s3 = null; - - await d.CollectAsync(); - Assert.AreEqual(3, d.SnapCount); - Assert.AreEqual(3, d.Test.GetValues(1).Length); - - GC.Collect(); - await d.CollectAsync(); - Assert.AreEqual(0, d.SnapCount); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - } - - [Retry(5)] // TODO make this test non-flaky. - [Test] - public async Task RandomTest1() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - d.Set(1, "one"); - d.Set(2, "two"); - - var s1 = d.CreateSnapshot(); - var v1 = s1.Get(1); - Assert.AreEqual("one", v1); - - d.Set(1, "uno"); - - var s2 = d.CreateSnapshot(); - var v2 = s2.Get(1); - Assert.AreEqual("uno", v2); - - v1 = s1.Get(1); - Assert.AreEqual("one", v1); - - Assert.AreEqual(2, d.SnapCount); - - s1 = null; - GC.Collect(); - await d.CollectAsync(); - - GC.Collect(); - await d.CollectAsync(); - - Assert.AreEqual(1, d.SnapCount); - v2 = s2.Get(1); - Assert.AreEqual("uno", v2); - - s2 = null; - GC.Collect(); - await d.CollectAsync(); - - Assert.AreEqual(0, d.SnapCount); - } - - [Retry(5)] // TODO make this test non-flaky. - [Test] - public async Task RandomTest2() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - d.Set(1, "one"); - d.Set(2, "two"); - - var s1 = d.CreateSnapshot(); - var v1 = s1.Get(1); - Assert.AreEqual("one", v1); - - d.Clear(1); - - var s2 = d.CreateSnapshot(); - var v2 = s2.Get(1); - Assert.AreEqual(null, v2); - - v1 = s1.Get(1); - Assert.AreEqual("one", v1); - - Assert.AreEqual(2, d.SnapCount); - - s1 = null; - GC.Collect(); - await d.CollectAsync(); - - GC.Collect(); - await d.CollectAsync(); - - Assert.AreEqual(1, d.SnapCount); - v2 = s2.Get(1); - Assert.AreEqual(null, v2); - - s2 = null; - GC.Collect(); - await d.CollectAsync(); - - Assert.AreEqual(0, d.SnapCount); - } - - [Test] - public void WriteLockingFirstSnapshot() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - // gen 1 - d.Set(1, "one"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - using (d.GetScopedWriteLock(GetScopeProvider())) - { - var s1 = d.CreateSnapshot(); - - Assert.AreEqual(0, s1.Gen); - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - Assert.IsNull(s1.Get(1)); - } - - var s2 = d.CreateSnapshot(); - - Assert.AreEqual(1, s2.Gen); - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - Assert.AreEqual("one", s2.Get(1)); - } - - [Test] - public void WriteLocking() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - // gen 1 - d.Set(1, "one"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s1 = d.CreateSnapshot(); - - Assert.AreEqual(1, s1.Gen); - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - Assert.AreEqual("one", s1.Get(1)); - - // gen 2 - Assert.AreEqual(1, d.Test.GetValues(1).Length); - d.Set(1, "uno"); - Assert.AreEqual(2, d.Test.GetValues(1).Length); - - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s2 = d.CreateSnapshot(); - - Assert.AreEqual(2, s2.Gen); - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - Assert.AreEqual("uno", s2.Get(1)); - - using (d.GetScopedWriteLock(GetScopeProvider())) - { - // gen 3 - Assert.AreEqual(2, d.Test.GetValues(1).Length); - d.SetLocked(1, "ein"); - Assert.AreEqual(3, d.Test.GetValues(1).Length); - - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s3 = d.CreateSnapshot(); - - Assert.AreEqual(2, s3.Gen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); // has NOT changed when (non) creating snapshot - Assert.AreEqual("uno", s3.Get(1)); - } - - var s4 = d.CreateSnapshot(); - - Assert.AreEqual(3, s4.Gen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - Assert.AreEqual("ein", s4.Get(1)); - } - - [Test] - public void NestedWriteLocking1() - { - var d = new SnapDictionary(); - var t = d.Test; - t.CollectAuto = false; - - Assert.AreEqual(0, d.CreateSnapshot().Gen); - - // no scope context: writers nest, last one to be disposed commits - var scopeProvider = GetScopeProvider(); - - using (var w1 = d.GetScopedWriteLock(scopeProvider)) - { - Assert.AreEqual(1, t.LiveGen); - Assert.IsTrue(t.IsLocked); - Assert.IsTrue(t.NextGen); - - Assert.Throws(() => - { - using (var w2 = d.GetScopedWriteLock(scopeProvider)) - { - } - }); - - Assert.AreEqual(1, t.LiveGen); - Assert.IsTrue(t.IsLocked); - Assert.IsTrue(t.NextGen); - - Assert.AreEqual(0, d.CreateSnapshot().Gen); - } - - Assert.AreEqual(1, t.LiveGen); - Assert.IsFalse(t.IsLocked); - Assert.IsTrue(t.NextGen); - - Assert.AreEqual(1, d.CreateSnapshot().Gen); - } - - [Test] - public void NestedWriteLocking2() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - Assert.AreEqual(0, d.CreateSnapshot().Gen); - - // scope context: writers enlist - var scopeContext = new ScopeContext(); - var scopeProvider = GetScopeProvider(scopeContext); - - using (var w1 = d.GetScopedWriteLock(scopeProvider)) - { - // This one is interesting, although we don't allow recursive locks, since this is - // using the same ScopeContext/key, the lock acquisition is only done once. - using (var w2 = d.GetScopedWriteLock(scopeProvider)) - { - Assert.AreSame(w1, w2); - - d.SetLocked(1, "one"); - } - } - } - - [Test] - public void NestedWriteLocking3() - { - var d = new SnapDictionary(); - var t = d.Test; - t.CollectAuto = false; - - Assert.AreEqual(0, d.CreateSnapshot().Gen); - - var scopeContext = new ScopeContext(); - var scopeProvider1 = GetScopeProvider(); - var scopeProvider2 = GetScopeProvider(scopeContext); - - using (var w1 = d.GetScopedWriteLock(scopeProvider1)) - { - Assert.AreEqual(1, t.LiveGen); - Assert.IsTrue(t.IsLocked); - Assert.IsTrue(t.NextGen); - - Assert.Throws(() => - { - using (var w2 = d.GetScopedWriteLock(scopeProvider2)) - { - } - }); - } - } - - [Test] - public void WriteLocking2() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - // gen 1 - d.Set(1, "one"); - Assert.AreEqual(1, d.Test.GetValues(1).Length); - - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s1 = d.CreateSnapshot(); - - Assert.AreEqual(1, s1.Gen); - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - Assert.AreEqual("one", s1.Get(1)); - - // gen 2 - Assert.AreEqual(1, d.Test.GetValues(1).Length); - d.Set(1, "uno"); - Assert.AreEqual(2, d.Test.GetValues(1).Length); - - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s2 = d.CreateSnapshot(); - - Assert.AreEqual(2, s2.Gen); - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - Assert.AreEqual("uno", s2.Get(1)); - - var scopeProvider = GetScopeProvider(); - using (d.GetScopedWriteLock(scopeProvider)) - { - // gen 3 - Assert.AreEqual(2, d.Test.GetValues(1).Length); - d.SetLocked(1, "ein"); - Assert.AreEqual(3, d.Test.GetValues(1).Length); - - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s3 = d.CreateSnapshot(); - - Assert.AreEqual(2, s3.Gen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); // has NOT changed when (non) creating snapshot - Assert.AreEqual("uno", s3.Get(1)); - } - - var s4 = d.CreateSnapshot(); - - Assert.AreEqual(3, s4.Gen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - Assert.AreEqual("ein", s4.Get(1)); - } - - [Test] - public void WriteLocking3() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - // gen 1 - d.Set(1, "one"); - var s1 = d.CreateSnapshot(); - Assert.AreEqual(1, s1.Gen); - Assert.AreEqual("one", s1.Get(1)); - - d.Set(1, "uno"); - var s2 = d.CreateSnapshot(); - Assert.AreEqual(2, s2.Gen); - Assert.AreEqual("uno", s2.Get(1)); - - var scopeProvider = GetScopeProvider(); - using (d.GetScopedWriteLock(scopeProvider)) - { - // creating a snapshot in a write-lock does NOT return the "current" content - // it uses the previous snapshot, so new snapshot created only on release - d.SetLocked(1, "ein"); - var s3 = d.CreateSnapshot(); - Assert.AreEqual(2, s3.Gen); - Assert.AreEqual("uno", s3.Get(1)); - - // but live snapshot contains changes - var ls = d.Test.LiveSnapshot; - Assert.AreEqual("ein", ls.Get(1)); - Assert.AreEqual(3, ls.Gen); - } - - var s4 = d.CreateSnapshot(); - Assert.AreEqual(3, s4.Gen); - Assert.AreEqual("ein", s4.Get(1)); - } - - [Test] - public void ScopeLocking1() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - // gen 1 - d.Set(1, "one"); - var s1 = d.CreateSnapshot(); - Assert.AreEqual(1, s1.Gen); - Assert.AreEqual("one", s1.Get(1)); - - d.Set(1, "uno"); - var s2 = d.CreateSnapshot(); - Assert.AreEqual(2, s2.Gen); - Assert.AreEqual("uno", s2.Get(1)); - - var scopeContext = new ScopeContext(); - var scopeProvider = GetScopeProvider(scopeContext); - using (d.GetScopedWriteLock(scopeProvider)) - { - // creating a snapshot in a write-lock does NOT return the "current" content - // it uses the previous snapshot, so new snapshot created only on release - d.SetLocked(1, "ein"); - var s3 = d.CreateSnapshot(); - Assert.AreEqual(2, s3.Gen); - Assert.AreEqual("uno", s3.Get(1)); - - // but live snapshot contains changes - var ls = d.Test.LiveSnapshot; - Assert.AreEqual("ein", ls.Get(1)); - Assert.AreEqual(3, ls.Gen); - } - - var s4 = d.CreateSnapshot(); - Assert.AreEqual(2, s4.Gen); - Assert.AreEqual("uno", s4.Get(1)); - - scopeContext.ScopeExit(true); - - var s5 = d.CreateSnapshot(); - Assert.AreEqual(3, s5.Gen); - Assert.AreEqual("ein", s5.Get(1)); - } - - [Test] - public void ScopeLocking2() - { - var d = new SnapDictionary(); - var t = d.Test; - t.CollectAuto = false; - - // gen 1 - d.Set(1, "one"); - var s1 = d.CreateSnapshot(); - Assert.AreEqual(1, s1.Gen); - Assert.AreEqual("one", s1.Get(1)); - - d.Set(1, "uno"); - var s2 = d.CreateSnapshot(); - Assert.AreEqual(2, s2.Gen); - Assert.AreEqual("uno", s2.Get(1)); - - Assert.AreEqual(2, t.LiveGen); - Assert.IsFalse(t.NextGen); - - var scopeContext = new ScopeContext(); - var scopeProvider = GetScopeProvider(scopeContext); - using (d.GetScopedWriteLock(scopeProvider)) - { - // creating a snapshot in a write-lock does NOT return the "current" content - // it uses the previous snapshot, so new snapshot created only on release - d.SetLocked(1, "ein"); - var s3 = d.CreateSnapshot(); - Assert.AreEqual(2, s3.Gen); - Assert.AreEqual("uno", s3.Get(1)); - - // we made some changes, so a next gen is required - Assert.AreEqual(3, t.LiveGen); - Assert.IsTrue(t.NextGen); - Assert.IsTrue(t.IsLocked); - - // but live snapshot contains changes - var ls = t.LiveSnapshot; - Assert.AreEqual("ein", ls.Get(1)); - Assert.AreEqual(3, ls.Gen); - } - - // nothing is committed until scope exits - Assert.AreEqual(3, t.LiveGen); - Assert.IsTrue(t.NextGen); - Assert.IsTrue(t.IsLocked); - - // no changes until exit - var s4 = d.CreateSnapshot(); - Assert.AreEqual(2, s4.Gen); - Assert.AreEqual("uno", s4.Get(1)); - - scopeContext.ScopeExit(false); - - // now things have changed - Assert.AreEqual(2, t.LiveGen); - Assert.IsFalse(t.NextGen); - Assert.IsFalse(t.IsLocked); - - // no changes since not completed - var s5 = d.CreateSnapshot(); - Assert.AreEqual(2, s5.Gen); - Assert.AreEqual("uno", s5.Get(1)); - } - - [Test] - public void GetAll() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - Assert.AreEqual(0, d.Test.GetValues(1).Length); - - d.Set(1, "one"); - d.Set(2, "two"); - d.Set(3, "three"); - d.Set(4, "four"); - - var s1 = d.CreateSnapshot(); - var all = s1.GetAll().ToArray(); - Assert.AreEqual(4, all.Length); - Assert.AreEqual("one", all[0]); - Assert.AreEqual("four", all[3]); - - d.Set(1, "uno"); - var s2 = d.CreateSnapshot(); - - all = s1.GetAll().ToArray(); - Assert.AreEqual(4, all.Length); - Assert.AreEqual("one", all[0]); - Assert.AreEqual("four", all[3]); - - all = s2.GetAll().ToArray(); - Assert.AreEqual(4, all.Length); - Assert.AreEqual("uno", all[0]); - Assert.AreEqual("four", all[3]); - } - - [Test] - public void DontPanic() - { - var d = new SnapDictionary(); - d.Test.CollectAuto = false; - - Assert.IsNull(d.Test.GenObj); - - // gen 1 - d.Set(1, "one"); - Assert.IsTrue(d.Test.NextGen); - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsNull(d.Test.GenObj); - - var s1 = d.CreateSnapshot(); - Assert.IsFalse(d.Test.NextGen); - Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsNotNull(d.Test.GenObj); - Assert.AreEqual(1, d.Test.GenObj.Gen); - - Assert.AreEqual(1, s1.Gen); - Assert.AreEqual("one", s1.Get(1)); - - d.Set(1, "uno"); - Assert.IsTrue(d.Test.NextGen); - Assert.AreEqual(2, d.Test.LiveGen); - Assert.IsNotNull(d.Test.GenObj); - Assert.AreEqual(1, d.Test.GenObj.Gen); - - var scopeContext = new ScopeContext(); - var scopeProvider = GetScopeProvider(scopeContext); - - // scopeProvider.Context == scopeContext -> writer is scoped - // writer is scope contextual and scoped - // when disposed, nothing happens - // when the context exists, the writer is released - using (d.GetScopedWriteLock(scopeProvider)) - { - d.SetLocked(1, "ein"); - Assert.IsTrue(d.Test.NextGen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsNotNull(d.Test.GenObj); - Assert.AreEqual(2, d.Test.GenObj.Gen); - } - - // writer has not released - Assert.IsTrue(d.Test.IsLocked); - Assert.IsNotNull(d.Test.GenObj); - Assert.AreEqual(2, d.Test.GenObj.Gen); - - // nothing changed - Assert.IsTrue(d.Test.NextGen); - Assert.AreEqual(3, d.Test.LiveGen); - - // panic! - var s2 = d.CreateSnapshot(); - - Assert.IsTrue(d.Test.IsLocked); - Assert.IsNotNull(d.Test.GenObj); - Assert.AreEqual(2, d.Test.GenObj.Gen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - // release writer - scopeContext.ScopeExit(true); - - Assert.IsFalse(d.Test.IsLocked); - Assert.IsNotNull(d.Test.GenObj); - Assert.AreEqual(2, d.Test.GenObj.Gen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsTrue(d.Test.NextGen); - - var s3 = d.CreateSnapshot(); - - Assert.IsFalse(d.Test.IsLocked); - Assert.IsNotNull(d.Test.GenObj); - Assert.AreEqual(3, d.Test.GenObj.Gen); - Assert.AreEqual(3, d.Test.LiveGen); - Assert.IsFalse(d.Test.NextGen); - } - - private ICoreScopeProvider GetScopeProvider(ScopeContext scopeContext = null) - { - var scopeProvider = Mock.Of(); - Mock.Get(scopeProvider) - .Setup(x => x.Context).Returns(scopeContext); - return scopeProvider; - } -} - -/// -/// Used for tests so that we don't have to wrap every Set/Clear call in locks -/// -public static class SnapDictionaryExtensions -{ - internal static void Set(this SnapDictionary d, TKey key, TValue value) - where TValue : class - { - using (d.GetScopedWriteLock(GetScopeProvider())) - { - d.SetLocked(key, value); - } - } - - internal static void Clear(this SnapDictionary d) - where TValue : class - { - using (d.GetScopedWriteLock(GetScopeProvider())) - { - d.ClearLocked(); - } - } - - internal static void Clear(this SnapDictionary d, TKey key) - where TValue : class - { - using (d.GetScopedWriteLock(GetScopeProvider())) - { - d.ClearLocked(key); - } - } - - private static ICoreScopeProvider GetScopeProvider() - { - var scopeProvider = Mock.Of(); - Mock.Get(scopeProvider) - .Setup(x => x.Context).Returns(() => null); - return scopeProvider; - } -} +// // Copyright (c) Umbraco. +// // See LICENSE for more details. +// +// using System.Linq; +// using System.Threading.Tasks; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Scoping; +// using Umbraco.Cms.Infrastructure.PublishedCache; +// +// namespace Umbraco.Cms.Tests.UnitTests.Umbraco.PublishedCache.NuCache; +// +// FIXME: Reintroduce if relevant +// [TestFixture] +// public class SnapDictionaryTests +// { +// [Test] +// public void LiveGenUpdate() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// Assert.AreEqual(0, d.Test.GetValues(1).Length); +// +// // gen 1 +// d.Set(1, "one"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// d.Clear(1); +// Assert.AreEqual(0, d.Test.GetValues(1).Length); // gone +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// Assert.AreEqual(0, d.Test.FloorGen); +// } +// +// [Test] +// public void OtherGenUpdate() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// Assert.AreEqual(0, d.Test.GetValues(1).Length); +// Assert.AreEqual(0, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// // gen 1 +// d.Set(1, "one"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s = d.CreateSnapshot(); +// Assert.AreEqual(1, s.Gen); +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// // gen 2 +// d.Clear(1); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); // there +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// Assert.AreEqual(0, d.Test.FloorGen); +// +// GC.KeepAlive(s); +// } +// +// [Test] +// public void MissingReturnsNull() +// { +// var d = new SnapDictionary(); +// var s = d.CreateSnapshot(); +// +// Assert.IsNull(s.Get(1)); +// } +// +// [Test] +// public void DeletedReturnsNull() +// { +// var d = new SnapDictionary(); +// +// // gen 1 +// d.Set(1, "one"); +// +// var s1 = d.CreateSnapshot(); +// Assert.AreEqual("one", s1.Get(1)); +// +// // gen 2 +// d.Clear(1); +// +// var s2 = d.CreateSnapshot(); +// Assert.IsNull(s2.Get(1)); +// +// Assert.AreEqual("one", s1.Get(1)); +// } +// +// [Retry(5)] // TODO make this test non-flaky. +// [Test] +// public async Task CollectValues() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// // gen 1 +// d.Set(1, "one"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// d.Set(1, "one"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// d.Set(1, "uno"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s1 = d.CreateSnapshot(); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// // gen 2 +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// d.Set(1, "one"); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// d.Set(1, "uno"); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s2 = d.CreateSnapshot(); +// +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// // gen 3 +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// d.Set(1, "one"); +// Assert.AreEqual(3, d.Test.GetValues(1).Length); +// d.Set(1, "uno"); +// Assert.AreEqual(3, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var tv = d.Test.GetValues(1); +// Assert.AreEqual(3, tv[0].Gen); +// Assert.AreEqual(2, tv[1].Gen); +// Assert.AreEqual(1, tv[2].Gen); +// +// Assert.AreEqual(0, d.Test.FloorGen); +// +// // nothing to collect +// await d.CollectAsync(); +// GC.KeepAlive(s1); +// GC.KeepAlive(s2); +// Assert.AreEqual(0, d.Test.FloorGen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// Assert.AreEqual(2, d.SnapCount); +// Assert.AreEqual(3, d.Test.GetValues(1).Length); +// +// // one snapshot to collect +// s1 = null; +// GC.Collect(); +// GC.KeepAlive(s2); +// await d.CollectAsync(); +// Assert.AreEqual(1, d.Test.FloorGen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// Assert.AreEqual(1, d.SnapCount); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// +// // another snapshot to collect +// s2 = null; +// GC.Collect(); +// await d.CollectAsync(); +// Assert.AreEqual(2, d.Test.FloorGen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// Assert.AreEqual(0, d.SnapCount); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// } +// +// [Test] +// public async Task ProperlyCollects() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// for (var i = 0; i < 32; i++) +// { +// d.Set(i, i.ToString()); +// d.CreateSnapshot().Dispose(); +// } +// +// Assert.AreEqual(32, d.GenCount); +// Assert.AreEqual(0, d.SnapCount); // because we've disposed them +// +// await d.CollectAsync(); +// Assert.AreEqual(32, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// Assert.AreEqual(0, d.GenCount); +// Assert.AreEqual(0, d.SnapCount); +// Assert.AreEqual(32, d.Count); +// +// for (var i = 0; i < 32; i++) +// { +// d.Set(i, null); +// } +// +// d.CreateSnapshot().Dispose(); +// +// // because we haven't collected yet, but disposed nevertheless +// Assert.AreEqual(1, d.GenCount); +// Assert.AreEqual(0, d.SnapCount); +// Assert.AreEqual(32, d.Count); +// +// // once we collect, they are all gone +// // since noone is interested anymore +// await d.CollectAsync(); +// Assert.AreEqual(0, d.GenCount); +// Assert.AreEqual(0, d.SnapCount); +// Assert.AreEqual(0, d.Count); +// } +// +// [Retry(5)] // TODO make this test non-flaky. +// [Test] +// public async Task CollectNulls() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// // gen 1 +// d.Set(1, "one"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// d.Set(1, "one"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// d.Set(1, "uno"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s1 = d.CreateSnapshot(); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// // gen 2 +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// d.Set(1, "one"); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// d.Set(1, "uno"); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s2 = d.CreateSnapshot(); +// +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// // gen 3 +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// d.Set(1, "one"); +// Assert.AreEqual(3, d.Test.GetValues(1).Length); +// d.Set(1, "uno"); +// Assert.AreEqual(3, d.Test.GetValues(1).Length); +// d.Clear(1); +// Assert.AreEqual(3, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var tv = d.Test.GetValues(1); +// Assert.AreEqual(3, tv[0].Gen); +// Assert.AreEqual(2, tv[1].Gen); +// Assert.AreEqual(1, tv[2].Gen); +// +// Assert.AreEqual(0, d.Test.FloorGen); +// +// // nothing to collect +// await d.CollectAsync(); +// GC.KeepAlive(s1); +// GC.KeepAlive(s2); +// Assert.AreEqual(0, d.Test.FloorGen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// Assert.AreEqual(2, d.SnapCount); +// Assert.AreEqual(3, d.Test.GetValues(1).Length); +// +// // one snapshot to collect +// s1 = null; +// GC.Collect(); +// GC.KeepAlive(s2); +// await d.CollectAsync(); +// Assert.AreEqual(1, d.Test.FloorGen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// Assert.AreEqual(1, d.SnapCount); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// +// // another snapshot to collect +// s2 = null; +// GC.Collect(); +// await d.CollectAsync(); +// Assert.AreEqual(2, d.Test.FloorGen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// Assert.AreEqual(0, d.SnapCount); +// +// // and everything is gone? +// // no, cannot collect the live gen because we'd need to lock +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// +// d.CreateSnapshot(); +// GC.Collect(); +// await d.CollectAsync(); +// +// // poof, gone +// Assert.AreEqual(0, d.Test.GetValues(1).Length); +// } +// +// [Test] +// [Retry(5)] // TODO make this test non-flaky. +// public async Task EventuallyCollectNulls() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// Assert.AreEqual(0, d.Test.GetValues(1).Length); +// +// // gen 1 +// d.Set(1, "one"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// await d.CollectAsync(); +// var tv = d.Test.GetValues(1); +// Assert.AreEqual(1, tv.Length); +// Assert.AreEqual(1, tv[0].Gen); +// +// var s = d.CreateSnapshot(); +// Assert.AreEqual("one", s.Get(1)); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// Assert.AreEqual(1, d.Count); +// Assert.AreEqual(1, d.SnapCount); +// Assert.AreEqual(1, d.GenCount); +// +// // gen 2 +// d.Clear(1); +// tv = d.Test.GetValues(1); +// Assert.AreEqual(2, tv.Length); +// Assert.AreEqual(2, tv[0].Gen); +// +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// Assert.AreEqual(1, d.Count); +// Assert.AreEqual(1, d.SnapCount); +// Assert.AreEqual(1, d.GenCount); +// +// // nothing to collect +// await d.CollectAsync(); +// GC.KeepAlive(s); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(1, d.Count); +// Assert.AreEqual(1, d.SnapCount); +// Assert.AreEqual(1, d.GenCount); +// +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// // collect snapshot +// // don't collect liveGen+ +// s = null; // without being disposed +// GC.Collect(); // should release the generation reference +// await d.CollectAsync(); +// +// Assert.AreEqual(1, d.Test.GetValues(1).Length); // "one" value is gone +// Assert.AreEqual(1, d.Count); // still have 1 item +// Assert.AreEqual(0, d.SnapCount); // snapshot is gone +// Assert.AreEqual(0, d.GenCount); // and generation has been dequeued +// +// // liveGen/nextGen +// s = d.CreateSnapshot(); +// s = null; +// +// // collect liveGen +// GC.Collect(); +// +// Assert.IsTrue(d.Test.GenObjs.TryPeek(out var genObj)); +// genObj = null; +// +// // in Release mode, it works, but in Debug mode, the weak reference is still alive +// // and for some reason we need to do this to ensure it is collected +// #if DEBUG +// await d.CollectAsync(); +// GC.Collect(); +// #endif +// +// Assert.IsTrue(d.Test.GenObjs.TryPeek(out genObj)); +// Assert.IsFalse(genObj.WeakGenRef.IsAlive); // snapshot is gone, along with its reference +// +// await d.CollectAsync(); +// +// Assert.AreEqual(0, d.Test.GetValues(1).Length); // null value is gone +// Assert.AreEqual(0, d.Count); // item is gone +// Assert.AreEqual(0, d.Test.GenObjs.Count); +// Assert.AreEqual(0, d.SnapCount); // snapshot is gone +// Assert.AreEqual(0, d.GenCount); // and generation has been dequeued +// } +// +// [Test] +// public async Task CollectDisposedSnapshots() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// // gen 1 +// d.Set(1, "one"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s1 = d.CreateSnapshot(); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// // gen 2 +// d.Set(1, "two"); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s2 = d.CreateSnapshot(); +// +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// // gen 3 +// d.Set(1, "three"); +// Assert.AreEqual(3, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s3 = d.CreateSnapshot(); +// +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// Assert.AreEqual(3, d.SnapCount); +// +// s1.Dispose(); +// await d.CollectAsync(); +// Assert.AreEqual(2, d.SnapCount); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// +// s2.Dispose(); +// await d.CollectAsync(); +// Assert.AreEqual(1, d.SnapCount); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// +// s3.Dispose(); +// await d.CollectAsync(); +// Assert.AreEqual(0, d.SnapCount); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// } +// +// [Retry(5)] // TODO make this test non-flaky. +// [Test] +// public async Task CollectGcSnapshots() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// // gen 1 +// d.Set(1, "one"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s1 = d.CreateSnapshot(); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// // gen 2 +// d.Set(1, "two"); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s2 = d.CreateSnapshot(); +// +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// // gen 3 +// d.Set(1, "three"); +// Assert.AreEqual(3, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s3 = d.CreateSnapshot(); +// +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// +// Assert.AreEqual(3, d.SnapCount); +// +// s1 = s2 = s3 = null; +// +// await d.CollectAsync(); +// Assert.AreEqual(3, d.SnapCount); +// Assert.AreEqual(3, d.Test.GetValues(1).Length); +// +// GC.Collect(); +// await d.CollectAsync(); +// Assert.AreEqual(0, d.SnapCount); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// } +// +// [Retry(5)] // TODO make this test non-flaky. +// [Test] +// public async Task RandomTest1() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// d.Set(1, "one"); +// d.Set(2, "two"); +// +// var s1 = d.CreateSnapshot(); +// var v1 = s1.Get(1); +// Assert.AreEqual("one", v1); +// +// d.Set(1, "uno"); +// +// var s2 = d.CreateSnapshot(); +// var v2 = s2.Get(1); +// Assert.AreEqual("uno", v2); +// +// v1 = s1.Get(1); +// Assert.AreEqual("one", v1); +// +// Assert.AreEqual(2, d.SnapCount); +// +// s1 = null; +// GC.Collect(); +// await d.CollectAsync(); +// +// GC.Collect(); +// await d.CollectAsync(); +// +// Assert.AreEqual(1, d.SnapCount); +// v2 = s2.Get(1); +// Assert.AreEqual("uno", v2); +// +// s2 = null; +// GC.Collect(); +// await d.CollectAsync(); +// +// Assert.AreEqual(0, d.SnapCount); +// } +// +// [Retry(5)] // TODO make this test non-flaky. +// [Test] +// public async Task RandomTest2() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// d.Set(1, "one"); +// d.Set(2, "two"); +// +// var s1 = d.CreateSnapshot(); +// var v1 = s1.Get(1); +// Assert.AreEqual("one", v1); +// +// d.Clear(1); +// +// var s2 = d.CreateSnapshot(); +// var v2 = s2.Get(1); +// Assert.AreEqual(null, v2); +// +// v1 = s1.Get(1); +// Assert.AreEqual("one", v1); +// +// Assert.AreEqual(2, d.SnapCount); +// +// s1 = null; +// GC.Collect(); +// await d.CollectAsync(); +// +// GC.Collect(); +// await d.CollectAsync(); +// +// Assert.AreEqual(1, d.SnapCount); +// v2 = s2.Get(1); +// Assert.AreEqual(null, v2); +// +// s2 = null; +// GC.Collect(); +// await d.CollectAsync(); +// +// Assert.AreEqual(0, d.SnapCount); +// } +// +// [Test] +// public void WriteLockingFirstSnapshot() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// // gen 1 +// d.Set(1, "one"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// using (d.GetScopedWriteLock(GetScopeProvider())) +// { +// var s1 = d.CreateSnapshot(); +// +// Assert.AreEqual(0, s1.Gen); +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// Assert.IsNull(s1.Get(1)); +// } +// +// var s2 = d.CreateSnapshot(); +// +// Assert.AreEqual(1, s2.Gen); +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// Assert.AreEqual("one", s2.Get(1)); +// } +// +// [Test] +// public void WriteLocking() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// // gen 1 +// d.Set(1, "one"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s1 = d.CreateSnapshot(); +// +// Assert.AreEqual(1, s1.Gen); +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// Assert.AreEqual("one", s1.Get(1)); +// +// // gen 2 +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// d.Set(1, "uno"); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s2 = d.CreateSnapshot(); +// +// Assert.AreEqual(2, s2.Gen); +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// Assert.AreEqual("uno", s2.Get(1)); +// +// using (d.GetScopedWriteLock(GetScopeProvider())) +// { +// // gen 3 +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// d.SetLocked(1, "ein"); +// Assert.AreEqual(3, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s3 = d.CreateSnapshot(); +// +// Assert.AreEqual(2, s3.Gen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); // has NOT changed when (non) creating snapshot +// Assert.AreEqual("uno", s3.Get(1)); +// } +// +// var s4 = d.CreateSnapshot(); +// +// Assert.AreEqual(3, s4.Gen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// Assert.AreEqual("ein", s4.Get(1)); +// } +// +// [Test] +// public void NestedWriteLocking1() +// { +// var d = new SnapDictionary(); +// var t = d.Test; +// t.CollectAuto = false; +// +// Assert.AreEqual(0, d.CreateSnapshot().Gen); +// +// // no scope context: writers nest, last one to be disposed commits +// var scopeProvider = GetScopeProvider(); +// +// using (var w1 = d.GetScopedWriteLock(scopeProvider)) +// { +// Assert.AreEqual(1, t.LiveGen); +// Assert.IsTrue(t.IsLocked); +// Assert.IsTrue(t.NextGen); +// +// Assert.Throws(() => +// { +// using (var w2 = d.GetScopedWriteLock(scopeProvider)) +// { +// } +// }); +// +// Assert.AreEqual(1, t.LiveGen); +// Assert.IsTrue(t.IsLocked); +// Assert.IsTrue(t.NextGen); +// +// Assert.AreEqual(0, d.CreateSnapshot().Gen); +// } +// +// Assert.AreEqual(1, t.LiveGen); +// Assert.IsFalse(t.IsLocked); +// Assert.IsTrue(t.NextGen); +// +// Assert.AreEqual(1, d.CreateSnapshot().Gen); +// } +// +// [Test] +// public void NestedWriteLocking2() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// Assert.AreEqual(0, d.CreateSnapshot().Gen); +// +// // scope context: writers enlist +// var scopeContext = new ScopeContext(); +// var scopeProvider = GetScopeProvider(scopeContext); +// +// using (var w1 = d.GetScopedWriteLock(scopeProvider)) +// { +// // This one is interesting, although we don't allow recursive locks, since this is +// // using the same ScopeContext/key, the lock acquisition is only done once. +// using (var w2 = d.GetScopedWriteLock(scopeProvider)) +// { +// Assert.AreSame(w1, w2); +// +// d.SetLocked(1, "one"); +// } +// } +// } +// +// [Test] +// public void NestedWriteLocking3() +// { +// var d = new SnapDictionary(); +// var t = d.Test; +// t.CollectAuto = false; +// +// Assert.AreEqual(0, d.CreateSnapshot().Gen); +// +// var scopeContext = new ScopeContext(); +// var scopeProvider1 = GetScopeProvider(); +// var scopeProvider2 = GetScopeProvider(scopeContext); +// +// using (var w1 = d.GetScopedWriteLock(scopeProvider1)) +// { +// Assert.AreEqual(1, t.LiveGen); +// Assert.IsTrue(t.IsLocked); +// Assert.IsTrue(t.NextGen); +// +// Assert.Throws(() => +// { +// using (var w2 = d.GetScopedWriteLock(scopeProvider2)) +// { +// } +// }); +// } +// } +// +// [Test] +// public void WriteLocking2() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// // gen 1 +// d.Set(1, "one"); +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s1 = d.CreateSnapshot(); +// +// Assert.AreEqual(1, s1.Gen); +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// Assert.AreEqual("one", s1.Get(1)); +// +// // gen 2 +// Assert.AreEqual(1, d.Test.GetValues(1).Length); +// d.Set(1, "uno"); +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s2 = d.CreateSnapshot(); +// +// Assert.AreEqual(2, s2.Gen); +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// Assert.AreEqual("uno", s2.Get(1)); +// +// var scopeProvider = GetScopeProvider(); +// using (d.GetScopedWriteLock(scopeProvider)) +// { +// // gen 3 +// Assert.AreEqual(2, d.Test.GetValues(1).Length); +// d.SetLocked(1, "ein"); +// Assert.AreEqual(3, d.Test.GetValues(1).Length); +// +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s3 = d.CreateSnapshot(); +// +// Assert.AreEqual(2, s3.Gen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); // has NOT changed when (non) creating snapshot +// Assert.AreEqual("uno", s3.Get(1)); +// } +// +// var s4 = d.CreateSnapshot(); +// +// Assert.AreEqual(3, s4.Gen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// Assert.AreEqual("ein", s4.Get(1)); +// } +// +// [Test] +// public void WriteLocking3() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// // gen 1 +// d.Set(1, "one"); +// var s1 = d.CreateSnapshot(); +// Assert.AreEqual(1, s1.Gen); +// Assert.AreEqual("one", s1.Get(1)); +// +// d.Set(1, "uno"); +// var s2 = d.CreateSnapshot(); +// Assert.AreEqual(2, s2.Gen); +// Assert.AreEqual("uno", s2.Get(1)); +// +// var scopeProvider = GetScopeProvider(); +// using (d.GetScopedWriteLock(scopeProvider)) +// { +// // creating a snapshot in a write-lock does NOT return the "current" content +// // it uses the previous snapshot, so new snapshot created only on release +// d.SetLocked(1, "ein"); +// var s3 = d.CreateSnapshot(); +// Assert.AreEqual(2, s3.Gen); +// Assert.AreEqual("uno", s3.Get(1)); +// +// // but live snapshot contains changes +// var ls = d.Test.LiveSnapshot; +// Assert.AreEqual("ein", ls.Get(1)); +// Assert.AreEqual(3, ls.Gen); +// } +// +// var s4 = d.CreateSnapshot(); +// Assert.AreEqual(3, s4.Gen); +// Assert.AreEqual("ein", s4.Get(1)); +// } +// +// [Test] +// public void ScopeLocking1() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// // gen 1 +// d.Set(1, "one"); +// var s1 = d.CreateSnapshot(); +// Assert.AreEqual(1, s1.Gen); +// Assert.AreEqual("one", s1.Get(1)); +// +// d.Set(1, "uno"); +// var s2 = d.CreateSnapshot(); +// Assert.AreEqual(2, s2.Gen); +// Assert.AreEqual("uno", s2.Get(1)); +// +// var scopeContext = new ScopeContext(); +// var scopeProvider = GetScopeProvider(scopeContext); +// using (d.GetScopedWriteLock(scopeProvider)) +// { +// // creating a snapshot in a write-lock does NOT return the "current" content +// // it uses the previous snapshot, so new snapshot created only on release +// d.SetLocked(1, "ein"); +// var s3 = d.CreateSnapshot(); +// Assert.AreEqual(2, s3.Gen); +// Assert.AreEqual("uno", s3.Get(1)); +// +// // but live snapshot contains changes +// var ls = d.Test.LiveSnapshot; +// Assert.AreEqual("ein", ls.Get(1)); +// Assert.AreEqual(3, ls.Gen); +// } +// +// var s4 = d.CreateSnapshot(); +// Assert.AreEqual(2, s4.Gen); +// Assert.AreEqual("uno", s4.Get(1)); +// +// scopeContext.ScopeExit(true); +// +// var s5 = d.CreateSnapshot(); +// Assert.AreEqual(3, s5.Gen); +// Assert.AreEqual("ein", s5.Get(1)); +// } +// +// [Test] +// public void ScopeLocking2() +// { +// var d = new SnapDictionary(); +// var t = d.Test; +// t.CollectAuto = false; +// +// // gen 1 +// d.Set(1, "one"); +// var s1 = d.CreateSnapshot(); +// Assert.AreEqual(1, s1.Gen); +// Assert.AreEqual("one", s1.Get(1)); +// +// d.Set(1, "uno"); +// var s2 = d.CreateSnapshot(); +// Assert.AreEqual(2, s2.Gen); +// Assert.AreEqual("uno", s2.Get(1)); +// +// Assert.AreEqual(2, t.LiveGen); +// Assert.IsFalse(t.NextGen); +// +// var scopeContext = new ScopeContext(); +// var scopeProvider = GetScopeProvider(scopeContext); +// using (d.GetScopedWriteLock(scopeProvider)) +// { +// // creating a snapshot in a write-lock does NOT return the "current" content +// // it uses the previous snapshot, so new snapshot created only on release +// d.SetLocked(1, "ein"); +// var s3 = d.CreateSnapshot(); +// Assert.AreEqual(2, s3.Gen); +// Assert.AreEqual("uno", s3.Get(1)); +// +// // we made some changes, so a next gen is required +// Assert.AreEqual(3, t.LiveGen); +// Assert.IsTrue(t.NextGen); +// Assert.IsTrue(t.IsLocked); +// +// // but live snapshot contains changes +// var ls = t.LiveSnapshot; +// Assert.AreEqual("ein", ls.Get(1)); +// Assert.AreEqual(3, ls.Gen); +// } +// +// // nothing is committed until scope exits +// Assert.AreEqual(3, t.LiveGen); +// Assert.IsTrue(t.NextGen); +// Assert.IsTrue(t.IsLocked); +// +// // no changes until exit +// var s4 = d.CreateSnapshot(); +// Assert.AreEqual(2, s4.Gen); +// Assert.AreEqual("uno", s4.Get(1)); +// +// scopeContext.ScopeExit(false); +// +// // now things have changed +// Assert.AreEqual(2, t.LiveGen); +// Assert.IsFalse(t.NextGen); +// Assert.IsFalse(t.IsLocked); +// +// // no changes since not completed +// var s5 = d.CreateSnapshot(); +// Assert.AreEqual(2, s5.Gen); +// Assert.AreEqual("uno", s5.Get(1)); +// } +// +// [Test] +// public void GetAll() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// Assert.AreEqual(0, d.Test.GetValues(1).Length); +// +// d.Set(1, "one"); +// d.Set(2, "two"); +// d.Set(3, "three"); +// d.Set(4, "four"); +// +// var s1 = d.CreateSnapshot(); +// var all = s1.GetAll().ToArray(); +// Assert.AreEqual(4, all.Length); +// Assert.AreEqual("one", all[0]); +// Assert.AreEqual("four", all[3]); +// +// d.Set(1, "uno"); +// var s2 = d.CreateSnapshot(); +// +// all = s1.GetAll().ToArray(); +// Assert.AreEqual(4, all.Length); +// Assert.AreEqual("one", all[0]); +// Assert.AreEqual("four", all[3]); +// +// all = s2.GetAll().ToArray(); +// Assert.AreEqual(4, all.Length); +// Assert.AreEqual("uno", all[0]); +// Assert.AreEqual("four", all[3]); +// } +// +// [Test] +// public void DontPanic() +// { +// var d = new SnapDictionary(); +// d.Test.CollectAuto = false; +// +// Assert.IsNull(d.Test.GenObj); +// +// // gen 1 +// d.Set(1, "one"); +// Assert.IsTrue(d.Test.NextGen); +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsNull(d.Test.GenObj); +// +// var s1 = d.CreateSnapshot(); +// Assert.IsFalse(d.Test.NextGen); +// Assert.AreEqual(1, d.Test.LiveGen); +// Assert.IsNotNull(d.Test.GenObj); +// Assert.AreEqual(1, d.Test.GenObj.Gen); +// +// Assert.AreEqual(1, s1.Gen); +// Assert.AreEqual("one", s1.Get(1)); +// +// d.Set(1, "uno"); +// Assert.IsTrue(d.Test.NextGen); +// Assert.AreEqual(2, d.Test.LiveGen); +// Assert.IsNotNull(d.Test.GenObj); +// Assert.AreEqual(1, d.Test.GenObj.Gen); +// +// var scopeContext = new ScopeContext(); +// var scopeProvider = GetScopeProvider(scopeContext); +// +// // scopeProvider.Context == scopeContext -> writer is scoped +// // writer is scope contextual and scoped +// // when disposed, nothing happens +// // when the context exists, the writer is released +// using (d.GetScopedWriteLock(scopeProvider)) +// { +// d.SetLocked(1, "ein"); +// Assert.IsTrue(d.Test.NextGen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsNotNull(d.Test.GenObj); +// Assert.AreEqual(2, d.Test.GenObj.Gen); +// } +// +// // writer has not released +// Assert.IsTrue(d.Test.IsLocked); +// Assert.IsNotNull(d.Test.GenObj); +// Assert.AreEqual(2, d.Test.GenObj.Gen); +// +// // nothing changed +// Assert.IsTrue(d.Test.NextGen); +// Assert.AreEqual(3, d.Test.LiveGen); +// +// // panic! +// var s2 = d.CreateSnapshot(); +// +// Assert.IsTrue(d.Test.IsLocked); +// Assert.IsNotNull(d.Test.GenObj); +// Assert.AreEqual(2, d.Test.GenObj.Gen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// // release writer +// scopeContext.ScopeExit(true); +// +// Assert.IsFalse(d.Test.IsLocked); +// Assert.IsNotNull(d.Test.GenObj); +// Assert.AreEqual(2, d.Test.GenObj.Gen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsTrue(d.Test.NextGen); +// +// var s3 = d.CreateSnapshot(); +// +// Assert.IsFalse(d.Test.IsLocked); +// Assert.IsNotNull(d.Test.GenObj); +// Assert.AreEqual(3, d.Test.GenObj.Gen); +// Assert.AreEqual(3, d.Test.LiveGen); +// Assert.IsFalse(d.Test.NextGen); +// } +// +// private ICoreScopeProvider GetScopeProvider(ScopeContext scopeContext = null) +// { +// var scopeProvider = Mock.Of(); +// Mock.Get(scopeProvider) +// .Setup(x => x.Context).Returns(scopeContext); +// return scopeProvider; +// } +// } +// +// /// +// /// Used for tests so that we don't have to wrap every Set/Clear call in locks +// /// +// public static class SnapDictionaryExtensions +// { +// internal static void Set(this SnapDictionary d, TKey key, TValue value) +// where TValue : class +// { +// using (d.GetScopedWriteLock(GetScopeProvider())) +// { +// d.SetLocked(key, value); +// } +// } +// +// internal static void Clear(this SnapDictionary d) +// where TValue : class +// { +// using (d.GetScopedWriteLock(GetScopeProvider())) +// { +// d.ClearLocked(); +// } +// } +// +// internal static void Clear(this SnapDictionary d, TKey key) +// where TValue : class +// { +// using (d.GetScopedWriteLock(GetScopeProvider())) +// { +// d.ClearLocked(key); +// } +// } +// +// private static ICoreScopeProvider GetScopeProvider() +// { +// var scopeProvider = Mock.Of(); +// Mock.Get(scopeProvider) +// .Setup(x => x.Context).Returns(() => null); +// return scopeProvider; +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs index b1dd0f006a..fab432c6e1 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs @@ -75,8 +75,6 @@ public class SurfaceControllerTests [Test] public void Can_Lookup_Content() { - var publishedSnapshot = new Mock(); - publishedSnapshot.Setup(x => x.Members).Returns(Mock.Of()); var content = new Mock(); content.Setup(x => x.Id).Returns(2); var backofficeSecurityAccessor = Mock.Of(); diff --git a/umbraco.sln b/umbraco.sln index f39087c5eb..74fcc8dc3f 100644 --- a/umbraco.sln +++ b/umbraco.sln @@ -89,8 +89,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Infrastructure", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.TestData", "tests\Umbraco.TestData\Umbraco.TestData.csproj", "{FB5676ED-7A69-492C-B802-E7B24144C0FC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.PublishedCache.NuCache", "src\Umbraco.PublishedCache.NuCache\Umbraco.PublishedCache.NuCache.csproj", "{F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Examine.Lucene", "src\Umbraco.Examine.Lucene\Umbraco.Examine.Lucene.csproj", "{0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.Website", "src\Umbraco.Web.Website\Umbraco.Web.Website.csproj", "{5A246D54-3109-4D2B-BE7D-FC0787D126AE}" @@ -235,12 +233,6 @@ Global {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.Build.0 = Release|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Release|Any CPU.Build.0 = Release|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.Release|Any CPU.ActiveCfg = Release|Any CPU From 1d5eb2113d9cc8cb6bff394e10f398556f405386 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:17:59 +0200 Subject: [PATCH 22/25] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 239df086ad..53bbdf4291 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 239df086adb01be2b70861e01a42a19738ae775f +Subproject commit 53bbdf4291b005e90ce3c399cb14bae52ea90ee3 From 5e4d15be9378c26a01b0bf971b7deb5a486526e9 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:22:19 +0200 Subject: [PATCH 23/25] bump version to 15.1.0 --- src/Umbraco.Web.UI.Client | 2 +- version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 53bbdf4291..7d5d6558e3 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 53bbdf4291b005e90ce3c399cb14bae52ea90ee3 +Subproject commit 7d5d6558e3532f18d1220983e11ee19fceea745d diff --git a/version.json b/version.json index 36cd9614b6..ad1510bf81 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "15.0.0-rc1", + "version": "15.1.0-rc1", "assemblyVersion": { "precision": "build" }, From 31399c3b1556a044e0e1cdfcf9dcc37bd56a2c6e Mon Sep 17 00:00:00 2001 From: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> Date: Tue, 1 Oct 2024 20:49:17 +0700 Subject: [PATCH 24/25] V15 Fixed the failing smoke tests in the pipeline v15 (#17158) * Fixed the failing tests of Member Group due to UI changes * Fixed the failing tests of Member due to UI changes * Fixed the failing tests of User due to UI changes * Fixed failing tests for Dictionary and Document Type * Updated tests due to test helper changes * Bumped version * Updated assert steps due to the response changes * Updated tests due to api helper changes * Updated tests due to UI changes * Fixed tests for delete partial view * Fixed tests * Added more waits * Updated assert steps * Fixed failing tests for Block Grid and Media * Added more waits * Added skip tests * Removed waits time * Updated assertion steps for User * Added todo * Updated tests due to api helper changes * Bumped version * Added skip tests --- .../package-lock.json | 9 +- .../Umbraco.Tests.AcceptanceTest/package.json | 2 +- .../ContentWithPropertyEditors.spec.ts | 14 +-- .../Content/CultureAndHostnames.spec.ts | 1 + .../BlockGrid/BlockGridEditor.spec.ts | 3 +- .../BlockListEditor/BlockListEditor.spec.ts | 2 +- .../DataType/DataTypeFolder.spec.ts | 2 +- .../Dictionary/Dictionary.spec.ts | 2 +- .../tests/DefaultConfig/Media/Media.spec.ts | 14 +-- .../Members/MemberGroups.spec.ts | 11 +-- .../DefaultConfig/Members/Members.spec.ts | 24 ++--- .../DocumentType/DocumentType.spec.ts | 3 +- .../DocumentTypeDesignTab.spec.ts | 2 +- .../DocumentTypeTemplatesTab.spec.ts | 3 +- .../Settings/PartialView/PartialView.spec.ts | 4 +- .../tests/DefaultConfig/Users/User.spec.ts | 88 +++++++++++-------- 16 files changed, 92 insertions(+), 92 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index bb227d8a12..6d220d887b 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.20", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.84", + "@umbraco/playwright-testhelpers": "^15.0.0-beta.2", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -64,10 +64,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "2.0.0-beta.84", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.84.tgz", - "integrity": "sha512-vH13Lg48knTkkLVTwhMXUKTOdjtmixFj0wF5Qhgb++13u4AVDb+oW+TbFwTjSYaLeNMraq5Uhwmto/XuJPs2Rw==", - "license": "MIT", + "version": "15.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.0-beta.2.tgz", + "integrity": "sha512-srSepVmHA6A13/ibryobPcMLnc+4DsGu7HHgtgLphLiar7DUcHurQGx+bbtx/Jv5X6jEY0IifVn7K06iwwuFmw==", "dependencies": { "@umbraco/json-models-builders": "2.0.20", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 30706ad5de..ed0e3da31c 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.20", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.84", + "@umbraco/playwright-testhelpers": "^15.0.0-beta.2", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts index 074a4840f0..9717568ca9 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts @@ -14,19 +14,11 @@ test.afterEach(async ({umbracoApi}) => { await umbracoApi.documentType.ensureNameNotExists(documentTypeName); }); -test('can create content with the Rich Text Editor datatype', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { +// TODO: Skip this test as TinyMCE is replaced by Tiptap. This test should be updated. +test.skip('can create content with the Rich Text Editor datatype', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange const dataTypeName = 'Richtext editor'; const contentText = 'This is Rich Text Editor content!'; - const expectedContentValue = { - blocks: { - contentData: [], - layout: {}, - propertyEditorAlias: 'Umbraco.TinyMCE', - settingsData: [], - }, - markup: '

' + contentText + '

', - }; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); await umbracoUi.goToBackOffice(); @@ -44,7 +36,7 @@ test('can create content with the Rich Text Editor datatype', {tag: '@smoke'}, a await umbracoUi.content.doesSuccessNotificationsHaveCount(2); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].value).toEqual(expectedContentValue); + expect(contentData.values[0].value.markup).toEqual('

' + contentText + '

'); }); // TODO: Remove skip when the front-end is ready. Currently it returns error when publishing a content diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts index 369bbb7692..d006dc8e07 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts @@ -49,6 +49,7 @@ test('can add a domain', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuForContent(contentName); await umbracoUi.content.clickCultureAndHostnamesButton(); + await umbracoUi.waitForTimeout(1000); await umbracoUi.content.clickAddNewDomainButton(); await umbracoUi.content.enterDomain(domainName); await umbracoUi.content.selectDomainLanguageOption(languageName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts index fa717aa183..8a334815a2 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts @@ -62,8 +62,7 @@ test('can delete a block grid editor', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.dataType.clickRootFolderCaretButton(); await umbracoUi.dataType.clickActionsMenuForDataType(blockGridEditorName); - await umbracoUi.dataType.clickDeleteExactButton(); - await umbracoUi.dataType.clickConfirmToDeleteButton(); + await umbracoUi.dataType.clickDeleteAndConfirmButton(); // Assert await umbracoUi.dataType.isSuccessNotificationVisible(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts index 24142bca82..4a96105462 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts @@ -62,7 +62,7 @@ test('can delete a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbr // Act await umbracoUi.dataType.clickRootFolderCaretButton(); await umbracoUi.dataType.clickActionsMenuForDataType(blockListEditorName); - await umbracoUi.dataType.clickDeleteExactButton(); + await umbracoUi.dataType.clickDeleteButton(); await umbracoUi.dataType.clickConfirmToDeleteButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts index 2b5ce0857d..83606f4da1 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts @@ -55,7 +55,7 @@ test('can delete a data type folder', {tag: '@smoke'}, async ({umbracoApi, umbra await umbracoUi.dataType.deleteDataTypeFolder(dataTypeFolderName); // Assert - expect(await umbracoApi.dataType.doesNameExist(dataTypeFolderName)).toBeFalsy(); + expect(await umbracoApi.dataType.doesFolderExist(dataTypeFolderName)).toBeFalsy(); }); test('can create a data type in a folder', async ({umbracoApi, umbracoUi}) => { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts index 68ad9ee39b..7581347e68 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts @@ -59,7 +59,7 @@ test('can create a dictionary item in a dictionary', {tag: '@smoke'}, async ({um // Act await umbracoUi.dictionary.clickActionsMenuForDictionary(parentDictionaryName); - await umbracoUi.dictionary.clickCreateDictionaryItemButton(); + await umbracoUi.dictionary.clickCreateButton(); await umbracoUi.dictionary.enterDictionaryName(dictionaryName); await umbracoUi.dictionary.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts index 8dcf57923a..50177c64ee 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts @@ -192,7 +192,7 @@ test('can trash a media item', async ({umbracoApi, umbracoUi}) => { await umbracoUi.media.clickConfirmTrashButton(); // Assert - await umbracoUi.media.isMediaItemVisibleInRecycleBin(mediaFileName); + await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName); expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeTruthy(); @@ -212,7 +212,8 @@ test('can restore a media item from the recycle bin', async ({umbracoApi, umbrac await umbracoUi.media.restoreMediaItem(mediaFileName); // Assert - await umbracoUi.media.isMediaItemVisibleInRecycleBin(mediaFileName, false); + await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName, false); + await umbracoUi.media.reloadMediaTree(); await umbracoUi.media.isTreeItemVisible(mediaFileName); expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeTruthy(); expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeFalsy(); @@ -229,11 +230,11 @@ test('can delete a media item from the recycle bin', async ({umbracoApi, umbraco await umbracoUi.media.goToSection(ConstantHelper.sections.media); // Act - await umbracoUi.media.isMediaItemVisibleInRecycleBin(mediaFileName); + await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName); await umbracoUi.media.deleteMediaItem(mediaFileName); // Assert - await umbracoUi.media.isMediaItemVisibleInRecycleBin(mediaFileName, false); + await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName, false); expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeFalsy(); }); @@ -246,12 +247,13 @@ test('can empty the recycle bin', async ({umbracoApi, umbracoUi}) => { await umbracoUi.media.goToSection(ConstantHelper.sections.media); // Act - await umbracoUi.media.isMediaItemVisibleInRecycleBin(mediaFileName); + await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName); await umbracoUi.media.clickEmptyRecycleBinButton(); await umbracoUi.media.clickConfirmEmptyRecycleBinButton(); // Assert - await umbracoUi.media.isMediaItemVisibleInRecycleBin(mediaFileName, false); + // TODO: fix it + //await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName, false); expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeFalsy(); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/MemberGroups.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/MemberGroups.spec.ts index 80ce20aff8..e71e463afb 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/MemberGroups.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/MemberGroups.spec.ts @@ -1,4 +1,4 @@ -import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {test} from '@umbraco/playwright-testhelpers'; import {expect} from "@playwright/test"; const memberGroupName = 'Test Member Group'; @@ -6,7 +6,7 @@ const memberGroupName = 'Test Member Group'; test.beforeEach(async ({umbracoApi, umbracoUi}) => { await umbracoApi.memberGroup.ensureNameNotExists(memberGroupName); await umbracoUi.goToBackOffice(); - await umbracoUi.memberGroup.goToSection(ConstantHelper.sections.members); + await umbracoUi.memberGroup.goToMemberGroups(); }); test.afterEach(async ({umbracoApi}) => { @@ -15,7 +15,6 @@ test.afterEach(async ({umbracoApi}) => { test('can create a member group', {tag: '@smoke'}, async ({page, umbracoApi, umbracoUi}) => { // Act - await umbracoUi.memberGroup.clickMemberGroupsTab(); await umbracoUi.memberGroup.clickMemberGroupCreateButton(); await umbracoUi.memberGroup.enterMemberGroupName(memberGroupName); await umbracoUi.memberGroup.clickSaveButton(); @@ -29,7 +28,6 @@ test('can create a member group', {tag: '@smoke'}, async ({page, umbracoApi, umb test('cannot create member group with empty name', async ({umbracoApi, umbracoUi}) => { // Act - await umbracoUi.memberGroup.clickMemberGroupsTab(); await umbracoUi.memberGroup.clickMemberGroupCreateButton(); await umbracoUi.memberGroup.clickSaveButton(); @@ -45,7 +43,6 @@ test.skip('cannot create member group with duplicate name', async ({umbracoApi, expect(await umbracoApi.memberGroup.doesNameExist(memberGroupName)).toBeTruthy(); // Act - await umbracoUi.memberGroup.clickMemberGroupsTab(); await umbracoUi.memberGroup.clickCreateButton(true); await umbracoUi.memberGroup.enterMemberGroupName(memberGroupName); await umbracoUi.memberGroup.clickSaveButton(); @@ -54,14 +51,12 @@ test.skip('cannot create member group with duplicate name', async ({umbracoApi, await umbracoUi.memberGroup.isErrorNotificationVisible(); }); -// TODO: Remove skip when the front-end is ready. Currently it is impossible to delete a member group. -test.skip('can delete a member group', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { +test('can delete a member group', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.memberGroup.create(memberGroupName); expect(await umbracoApi.memberGroup.doesNameExist(memberGroupName)).toBeTruthy(); // Act - await umbracoUi.memberGroup.clickMemberGroupsTab(); await umbracoUi.memberGroup.clickMemberGroupLinkByName(memberGroupName); await umbracoUi.memberGroup.clickActionsButton(); await umbracoUi.memberGroup.clickDeleteButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts index f44a164a20..df8ec8cce2 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts @@ -24,7 +24,7 @@ test.afterEach(async ({umbracoApi}) => { test('can create a member', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange - await umbracoUi.member.goToSection(ConstantHelper.sections.members); + await umbracoUi.member.goToMembers(); // Act await umbracoUi.member.clickCreateButton(); @@ -46,7 +46,7 @@ test('can edit comments', async ({umbracoApi, umbracoUi}) => { // Arrange const defaultMemberTypeData = await umbracoApi.memberType.getByName(defaultMemberTypeName); memberId = await umbracoApi.member.createDefaultMember(memberName, defaultMemberTypeData.id, email, username, password); - await umbracoUi.member.goToSection(ConstantHelper.sections.members); + await umbracoUi.member.goToMembers(); // Act await umbracoUi.member.clickMemberLinkByName(memberName); @@ -65,7 +65,7 @@ test('can edit username', async ({umbracoApi, umbracoUi}) => { const updatedUsername = 'updatedusername'; memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName); memberId = await umbracoApi.member.createDefaultMember(memberName, memberTypeId, email, username, password); - await umbracoUi.member.goToSection(ConstantHelper.sections.members); + await umbracoUi.member.goToMembers(); // Act await umbracoUi.member.clickMemberLinkByName(memberName); @@ -83,7 +83,7 @@ test('can edit email', async ({umbracoApi, umbracoUi}) => { const updatedEmail = 'updated@acceptance.test'; memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName); memberId = await umbracoApi.member.createDefaultMember(memberName, memberTypeId, email, username, password); - await umbracoUi.member.goToSection(ConstantHelper.sections.members); + await umbracoUi.member.goToMembers(); // Act await umbracoUi.member.clickMemberLinkByName(memberName); @@ -101,7 +101,7 @@ test('can edit password', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { const updatedPassword = '9876543210'; memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName); memberId = await umbracoApi.member.createDefaultMember(memberName, memberTypeId, email, username, password); - await umbracoUi.member.goToSection(ConstantHelper.sections.members); + await umbracoUi.member.goToMembers(); // Act await umbracoUi.member.clickMemberLinkByName(memberName); @@ -121,7 +121,7 @@ test('can add member group', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => const memberGroupId = await umbracoApi.memberGroup.create(memberGroupName); memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName); memberId = await umbracoApi.member.createDefaultMember(memberName, memberTypeId, email, username, password); - await umbracoUi.member.goToSection(ConstantHelper.sections.members); + await umbracoUi.member.goToMembers(); // Act await umbracoUi.member.clickMemberLinkByName(memberName); @@ -144,7 +144,7 @@ test('can remove member group', async ({umbracoApi, umbracoUi}) => { const memberGroupId = await umbracoApi.memberGroup.create(memberGroupName); memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName); memberId = await umbracoApi.member.createMemberWithMemberGroup(memberName, memberTypeId, email, username, password, memberGroupId); - await umbracoUi.member.goToSection(ConstantHelper.sections.members); + await umbracoUi.member.goToMembers(); // Act await umbracoUi.member.clickMemberLinkByName(memberName); @@ -165,7 +165,7 @@ test('can view member info', async ({umbracoApi, umbracoUi}) => { // Arrange memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName); memberId = await umbracoApi.member.createDefaultMember(memberName, memberTypeId, email, username, password); - await umbracoUi.member.goToSection(ConstantHelper.sections.members); + await umbracoUi.member.goToMembers(); // Act await umbracoUi.member.clickMemberLinkByName(memberName); @@ -189,7 +189,7 @@ test('can enable approved', async ({umbracoApi, umbracoUi}) => { // Arrange memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName); memberId = await umbracoApi.member.createDefaultMember(memberName, memberTypeId, email, username, password); - await umbracoUi.member.goToSection(ConstantHelper.sections.members); + await umbracoUi.member.goToMembers(); // Act await umbracoUi.member.clickMemberLinkByName(memberName); @@ -206,7 +206,7 @@ test('can delete member', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName); memberId = await umbracoApi.member.createDefaultMember(memberName, memberTypeId, email, username, password); - await umbracoUi.member.goToSection(ConstantHelper.sections.members); + await umbracoUi.member.goToMembers(); // Act await umbracoUi.member.clickMemberLinkByName(memberName); @@ -222,7 +222,7 @@ test('can delete member', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { test('cannot create member with invalid email', async ({umbracoApi, umbracoUi}) => { // Arrange const invalidEmail = 'invalidemail'; - await umbracoUi.member.goToSection(ConstantHelper.sections.members); + await umbracoUi.member.goToMembers(); // Act await umbracoUi.member.clickCreateButton(); @@ -246,7 +246,7 @@ test.skip('cannot update email to an invalid email', async ({umbracoApi, umbraco const invalidEmail = 'invalidemail'; memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName); memberId = await umbracoApi.member.createDefaultMember(memberName, memberTypeId, email, username, password); - await umbracoUi.member.goToSection(ConstantHelper.sections.members); + await umbracoUi.member.goToMembers(); // Act await umbracoUi.member.clickMemberLinkByName(memberName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentType.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentType.spec.ts index e12749b1cd..9ebe8d0062 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentType.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentType.spec.ts @@ -141,8 +141,7 @@ test('can delete a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoU // Act await umbracoUi.documentType.clickRootFolderCaretButton(); await umbracoUi.documentType.clickActionsMenuForDocumentType(documentTypeName); - await umbracoUi.documentType.clickDeleteExactButton(); - await umbracoUi.documentType.clickConfirmToDeleteButton(); + await umbracoUi.documentType.clickDeleteAndConfirmButton(); // Assert await umbracoUi.documentType.isSuccessNotificationVisible(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts index eb87b7288a..3f19d68085 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts @@ -397,7 +397,7 @@ test('can enable validation for a property in a document type', async ({umbracoA test('can allow vary by culture for a property in a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, groupName, true); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, groupName, false); await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); // Act diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeTemplatesTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeTemplatesTab.spec.ts index 1390bbdc4f..6a5aa65ad5 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeTemplatesTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeTemplatesTab.spec.ts @@ -37,7 +37,8 @@ test('can add an allowed template to a document type', {tag: '@smoke'}, async ({ await umbracoApi.template.ensureNameNotExists(templateName); }); -test('can set an allowed template as default for document type', async ({umbracoApi, umbracoUi}) => { +// TODO: Need to uodate Act steps +test.skip('can set an allowed template as default for document type', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.template.ensureNameNotExists(templateName); const templateId = await umbracoApi.template.createDefaultTemplate(templateName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts index 9b1ad8f8be..feb0f975ac 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts @@ -235,7 +235,7 @@ test.skip('can insert value into a partial view', async ({umbracoApi, umbracoUi} }); test('can delete a partial view', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { - //Arrange + // Arrange await umbracoApi.partialView.create(partialViewFileName, partialViewFileName, '/'); expect(await umbracoApi.partialView.doesExist(partialViewFileName)).toBeTruthy(); @@ -249,7 +249,7 @@ test('can delete a partial view', {tag: '@smoke'}, async ({umbracoApi, umbracoUi expect(await umbracoApi.partialView.doesExist(partialViewFileName)).toBeFalsy(); // Verify the partial view is NOT displayed under the Partial Views section await umbracoUi.partialView.clickRootFolderCaretButton(); - await umbracoUi.partialView.isPartialViewRootTreeItemVisible(partialViewFileName, false); + await umbracoUi.partialView.isPartialViewRootTreeItemVisible(partialViewFileName, false, false); }); // TODO: Remove skip when the front-end is ready. Currently the returned items count is not updated after choosing the root content. diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts index 87c69bb6a9..a1b2494350 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts @@ -1,4 +1,4 @@ -import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {test} from '@umbraco/playwright-testhelpers'; import {expect} from '@playwright/test'; const nameOfTheUser = 'TestUser'; @@ -16,10 +16,11 @@ test.afterEach(async ({umbracoApi}) => { test('can create a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickCreateButton(); + await umbracoUi.user.clickUserButton(); await umbracoUi.user.enterNameOfTheUser(nameOfTheUser); await umbracoUi.user.enterUserEmail(userEmail); await umbracoUi.user.clickChooseButton(); @@ -38,7 +39,7 @@ test('can rename a user', async ({umbracoApi, umbracoUi}) => { await umbracoApi.user.ensureNameNotExists(wrongName); const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(wrongName, wrongName + userEmail, [userGroup.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(wrongName); @@ -54,10 +55,11 @@ test('can delete a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickActionButton(); await umbracoUi.user.clickDeleteButton(); await umbracoUi.user.clickConfirmToDeleteButton(); @@ -65,7 +67,7 @@ test('can delete a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { await umbracoUi.user.isSuccessNotificationVisible(); expect(await umbracoApi.user.doesNameExist(nameOfTheUser)).toBeFalsy(); // Checks if the user is deleted from the list - await umbracoUi.user.clickUsersTabButton(); + await umbracoUi.user.clickUsersMenu(); await umbracoUi.user.isUserVisible(nameOfTheUser, false); }); @@ -75,7 +77,7 @@ test('can add multiple user groups to a user', async ({umbracoApi, umbracoUi}) = const userGroupWriters = await umbracoApi.userGroup.getByName(defaultUserGroupName); const userGroupTranslators = await umbracoApi.userGroup.getByName(secondUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroupWriters.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -86,7 +88,7 @@ test('can add multiple user groups to a user', async ({umbracoApi, umbracoUi}) = // Assert await umbracoUi.user.isSuccessNotificationVisible(); - const userData = await umbracoApi.user.getByName(nameOfTheUser); + await umbracoApi.user.getByName(nameOfTheUser); expect(await umbracoApi.user.doesUserContainUserGroupIds(nameOfTheUser, [userGroupWriters.id, userGroupTranslators.id])).toBeTruthy(); }); @@ -94,7 +96,7 @@ test('can remove a user group from a user', {tag: '@smoke'}, async ({umbracoApi, // Arrange const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -113,11 +115,11 @@ test('can update culture for a user', async ({umbracoApi, umbracoUi}) => { const danishIsoCode = 'da-dk'; const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); - await umbracoUi.user.selectUserLanguage('Dansk'); + await umbracoUi.user.selectUserLanguage('Dansk (Danmark)'); await umbracoUi.user.clickSaveButton(); // Assert @@ -136,7 +138,7 @@ test('can add a content start node to a user', {tag: '@smoke'}, async ({umbracoA await umbracoApi.documentType.ensureNameNotExists(documentTypeName); const documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName); const documentId = await umbracoApi.document.createDefaultDocument(documentName, documentTypeId); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -171,7 +173,7 @@ test('can add multiple content start nodes for a user', async ({umbracoApi, umbr userData.documentStartNodeIds.push({id: documentId}); await umbracoApi.user.update(userId, userData); const secondDocumentId = await umbracoApi.document.createDefaultDocument(secondDocumentName, documentTypeId); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -205,7 +207,7 @@ test('can remove a content start node from a user', {tag: '@smoke'}, async ({umb userData.documentStartNodeIds.push({id: documentId}); await umbracoApi.user.update(userId, userData); expect(await umbracoApi.user.doesUserContainContentStartNodeIds(nameOfTheUser, [documentId])).toBeTruthy(); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -229,7 +231,7 @@ test('can add media start nodes for a user', {tag: '@smoke'}, async ({umbracoApi await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); await umbracoApi.media.ensureNameNotExists(mediaName); const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -261,7 +263,7 @@ test('can add multiple media start nodes for a user', async ({umbracoApi, umbrac userData.mediaStartNodeIds.push({id: firstMediaId}); await umbracoApi.user.update(userId, userData); expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [firstMediaId])).toBeTruthy(); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -291,7 +293,7 @@ test('can remove a media start node from a user', async ({umbracoApi, umbracoUi} userData.mediaStartNodeIds.push({id: mediaId}); await umbracoApi.user.update(userId, userData); expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeTruthy(); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -311,7 +313,7 @@ test('can allow access to all documents for a user', async ({umbracoApi, umbraco // Arrange const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -328,7 +330,7 @@ test('can allow access to all media for a user', async ({umbracoApi, umbracoUi}) // Arrange const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -356,7 +358,7 @@ test('can see if the user has the correct access based on content start nodes', userData.documentStartNodeIds.push({id: documentId}); await umbracoApi.user.update(userId, userData); expect(await umbracoApi.user.doesUserContainContentStartNodeIds(nameOfTheUser, [documentId])).toBeTruthy(); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -381,7 +383,7 @@ test('can see if the user has the correct access based on media start nodes', as userData.mediaStartNodeIds.push({id: mediaId}); await umbracoApi.user.update(userId, userData); expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeTruthy(); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -398,10 +400,11 @@ test('can change password for a user', {tag: '@smoke'}, async ({umbracoApi, umbr const userPassword = 'TestPassword'; const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickActionButton(); await umbracoUi.user.clickChangePasswordButton(); await umbracoUi.user.updatePassword(userPassword); @@ -414,10 +417,11 @@ test('can disable a user', async ({umbracoApi, umbracoUi}) => { const disabledStatus = 'Disabled'; const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickActionButton(); await umbracoUi.user.clickDisableButton(); await umbracoUi.user.clickConfirmDisableButton(); @@ -434,10 +438,11 @@ test('can enable a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); await umbracoApi.user.disable([userId]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickActionButton(); await umbracoUi.user.clickEnableButton(); await umbracoUi.user.clickConfirmEnableButton(); @@ -454,7 +459,7 @@ test('can add an avatar to a user', {tag: '@smoke'}, async ({umbracoApi, umbraco const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); const filePath = './fixtures/mediaLibrary/Umbraco.png'; - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -471,7 +476,7 @@ test('can remove an avatar from a user', async ({umbracoApi, umbracoUi}) => { const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); await umbracoApi.user.addDefaultAvatarImageToUser(userId); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); @@ -487,7 +492,7 @@ test('can see if the inactive label is removed from the admin user', async ({umb // Arrange const userLabel = 'Active'; const currentUser = await umbracoApi.user.getCurrentUser(); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + await umbracoUi.user.goToUsers(); // Act await umbracoUi.user.clickUserWithName(currentUser.name); @@ -502,16 +507,18 @@ test('can search for a user', async ({umbracoApi, umbracoUi}) => { // Arrange const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + const totalUsers = await umbracoApi.user.getUsersCount(); + await umbracoUi.user.goToUsers(); // Act - await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.doesUserSectionContainUserAmount(totalUsers); await umbracoUi.user.searchInUserSection(nameOfTheUser); // Assert // Wait for filtering to be done await umbracoUi.waitForTimeout(200); - await umbracoUi.user.doesUserSectionContainUserAmount(1); + const userData = await umbracoApi.user.filterByText(nameOfTheUser); + await umbracoUi.user.doesUserSectionContainUserAmount(userData.total); await umbracoUi.user.doesUserSectionContainUserWithText(nameOfTheUser); }); @@ -520,16 +527,18 @@ test('can filter by status', async ({umbracoApi, umbracoUi}) => { const inactiveStatus = 'Inactive'; const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + const totalUsers = await umbracoApi.user.getUsersCount(); + await umbracoUi.user.goToUsers(); // Act - await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.doesUserSectionContainUserAmount(totalUsers); await umbracoUi.user.filterByStatusName(inactiveStatus); // Assert // Wait for filtering to be done await umbracoUi.waitForTimeout(200); - await umbracoUi.user.doesUserSectionContainUserAmount(1); + const userData = await umbracoApi.user.filterByUserStates(inactiveStatus); + await umbracoUi.user.doesUserSectionContainUserAmount(userData.total); await umbracoUi.user.doesUserSectionContainUserWithText(nameOfTheUser); await umbracoUi.user.doesUserSectionContainUserWithText(inactiveStatus); }); @@ -538,16 +547,18 @@ test('can filter by user groups', async ({umbracoApi, umbracoUi}) => { // Arrange const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + const totalUsers = await umbracoApi.user.getUsersCount(); + await umbracoUi.user.goToUsers(); // Act - await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.doesUserSectionContainUserAmount(totalUsers); await umbracoUi.user.filterByGroupName(defaultUserGroupName); // Assert // Wait for filtering to be done await umbracoUi.waitForTimeout(200); - await umbracoUi.user.doesUserSectionContainUserAmount(1); + const userData = await umbracoApi.user.filterByUserGroupIds(userGroup.id); + await umbracoUi.user.doesUserSectionContainUserAmount(userData.total); await umbracoUi.user.doesUserSectionContainUserWithText(defaultUserGroupName); }); @@ -555,16 +566,17 @@ test('can order by newest user', async ({umbracoApi, umbracoUi}) => { // Arrange const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - await umbracoUi.user.goToSection(ConstantHelper.sections.users); + const totalUsers = await umbracoApi.user.getUsersCount(); + await umbracoUi.user.goToUsers(); // Act - await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.doesUserSectionContainUserAmount(totalUsers); await umbracoUi.user.orderByNewestUser(); // Assert // Wait for filtering to be done await umbracoUi.waitForTimeout(200); - await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.doesUserSectionContainUserAmount(totalUsers); await umbracoUi.user.isUserWithNameTheFirstUserInList(nameOfTheUser); }); From 6985187d4d5ab00a8f684f19617e3a684a665d7b Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 2 Oct 2024 09:08:30 +0200 Subject: [PATCH 25/25] Update dotnet version in template (#17170) --- templates/UmbracoProject/.template.config/template.json | 8 ++++++-- templates/UmbracoProject/UmbracoProject.csproj | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/templates/UmbracoProject/.template.config/template.json b/templates/UmbracoProject/.template.config/template.json index e992165f0d..d9ae3a078e 100644 --- a/templates/UmbracoProject/.template.config/template.json +++ b/templates/UmbracoProject/.template.config/template.json @@ -108,15 +108,19 @@ "type": "generated", "generator": "switch", "datatype": "text", - "description": "Not relevant at the moment, but if we need to change the dotnet version based on the Umbraco version, we can do it here", + "description": "Used to calculate the dotnet version to use, for latest we want to use dotnet 9 and for LTS we want to use dotnet 8", "replaces": "DOTNET_VERSION_FROM_TEMPLATE", "parameters": { "evaluator": "C++", "datatype": "text", "cases": [ { - "condition": "(true)", + "condition": "(UmbracoRelease == 'Latest')", "value": "net9.0" + }, + { + "condition": "(UmbracoRelease == 'LTS')", + "value": "net8.0" } ] } diff --git a/templates/UmbracoProject/UmbracoProject.csproj b/templates/UmbracoProject/UmbracoProject.csproj index d411fd4acc..019d5d2990 100644 --- a/templates/UmbracoProject/UmbracoProject.csproj +++ b/templates/UmbracoProject/UmbracoProject.csproj @@ -1,6 +1,5 @@ - net9.0 DOTNET_VERSION_FROM_TEMPLATE enable enable