From 0aaac78cfa119fd4ff23eba9b3e92d0bd8510358 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 3 Jun 2024 11:23:25 +0200 Subject: [PATCH] A bunch of minor performance optimizations (#16335) * Do not execute query if no macros found * Request cache the permission lookup * Unbreak change by adding obsolete ctor * Clean up * Wrap indexing for delivery API in a scope * Do not ask options every time for the timeout, instead listen for updates * Lookup content types once instead of one by one * Use TryGetValue instead * Do a distinct on user ids before building index, to avoid issue with more than 2100 parameters * Don't map ContentDto (it's unused) * Introduce request bound block editor element cache --------- Co-authored-by: kjac --- ...ServerEFCoreDistributedLockingMechanism.cs | 22 +++++----- ...SqliteEFCoreDistributedLockingMechanism.cs | 18 ++++---- .../SqlServerDistributedLockingMechanism.cs | 19 ++++---- .../SqliteDistributedLockingMechanism.cs | 18 ++++---- .../ContentEditing/ContentItemDisplay.cs | 1 + src/Umbraco.Core/Services/UserService.cs | 43 +++++++++++++++---- .../BlockEditorElementTypeCache.cs | 32 ++++++++++++++ .../IBlockEditorElementTypeCache.cs | 8 ++++ .../UmbracoBuilder.CoreServices.cs | 3 ++ .../Examine/ContentValueSetBuilder.cs | 4 +- .../DeliveryApiContentIndexValueSetBuilder.cs | 36 +++++++++++++++- .../PropertyEditors/BlockEditorValidator.cs | 5 ++- .../BlockEditorValidatorBase.cs | 11 ++--- .../PropertyEditors/BlockEditorValues.cs | 22 +++++----- .../BlockGridPropertyEditorBase.cs | 7 +-- .../BlockListPropertyEditorBase.cs | 7 +-- .../RichTextEditorBlockValidator.cs | 5 ++- .../PropertyEditors/RichTextPropertyEditor.cs | 11 ++--- .../Templates/HtmlMacroParameterParser.cs | 19 ++++---- .../Mapping/ContentMapDefinition.cs | 13 ++---- .../DataValueEditorReuseTests.cs | 3 +- 21 files changed, 210 insertions(+), 97 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs create mode 100644 src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs diff --git a/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs index 4652c513a3..38cdeef114 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs @@ -16,8 +16,8 @@ namespace Umbraco.Cms.Persistence.EFCore.Locking; internal class SqlServerEFCoreDistributedLockingMechanism : IDistributedLockingMechanism where T : DbContext { - private readonly IOptionsMonitor _connectionStrings; - private readonly IOptionsMonitor _globalSettings; + private ConnectionStrings _connectionStrings; + private GlobalSettings _globalSettings; private readonly ILogger> _logger; private readonly Lazy> _scopeAccessor; // Hooray it's a circular dependency. @@ -32,27 +32,29 @@ internal class SqlServerEFCoreDistributedLockingMechanism : IDistributedLocki { _logger = logger; _scopeAccessor = scopeAccessor; - _globalSettings = globalSettings; - _connectionStrings = connectionStrings; + _globalSettings = globalSettings.CurrentValue; + _connectionStrings = connectionStrings.CurrentValue; + globalSettings.OnChange(x=>_globalSettings = x); + connectionStrings.OnChange(x=>_connectionStrings = x); } public bool HasActiveRelatedScope => _scopeAccessor.Value.AmbientScope is not null; /// - public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && - string.Equals(_connectionStrings.CurrentValue.ProviderName, "Microsoft.Data.SqlClient", StringComparison.InvariantCultureIgnoreCase) && _scopeAccessor.Value.AmbientScope is not null; + public bool Enabled => _connectionStrings.IsConnectionStringConfigured() && + string.Equals(_connectionStrings.ProviderName, "Microsoft.Data.SqlClient", StringComparison.InvariantCultureIgnoreCase) && _scopeAccessor.Value.AmbientScope is not null; /// public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout; return new SqlServerDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value); } /// public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout; return new SqlServerDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value); } @@ -168,9 +170,7 @@ internal class SqlServerEFCoreDistributedLockingMechanism : IDistributedLocki "A transaction with minimum ReadCommitted isolation level is required."); } - await dbContext.Database.ExecuteSqlRawAsync($"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};"); - - var rowsAffected = await dbContext.Database.ExecuteSqlAsync(@$"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}"); + var rowsAffected = await dbContext.Database.ExecuteSqlAsync(@$"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}"); if (rowsAffected == 0) { diff --git a/src/Umbraco.Cms.Persistence.EFCore/Locking/SqliteEFCoreDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.EFCore/Locking/SqliteEFCoreDistributedLockingMechanism.cs index 8d92ec0e03..23b3d8d410 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Locking/SqliteEFCoreDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/Locking/SqliteEFCoreDistributedLockingMechanism.cs @@ -16,8 +16,8 @@ namespace Umbraco.Cms.Persistence.EFCore.Locking; internal class SqliteEFCoreDistributedLockingMechanism : IDistributedLockingMechanism where T : DbContext { - private readonly IOptionsMonitor _connectionStrings; - private readonly IOptionsMonitor _globalSettings; + private ConnectionStrings _connectionStrings; + private GlobalSettings _globalSettings; private readonly ILogger> _logger; private readonly Lazy> _efCoreScopeAccessor; @@ -29,27 +29,29 @@ internal class SqliteEFCoreDistributedLockingMechanism : IDistributedLockingM { _logger = logger; _efCoreScopeAccessor = efCoreScopeAccessor; - _connectionStrings = connectionStrings; - _globalSettings = globalSettings; + _globalSettings = globalSettings.CurrentValue; + _connectionStrings = connectionStrings.CurrentValue; + globalSettings.OnChange(x=>_globalSettings = x); + connectionStrings.OnChange(x=>_connectionStrings = x); } public bool HasActiveRelatedScope => _efCoreScopeAccessor.Value.AmbientScope is not null; /// - public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && - string.Equals(_connectionStrings.CurrentValue.ProviderName, "Microsoft.Data.Sqlite", StringComparison.InvariantCultureIgnoreCase) && _efCoreScopeAccessor.Value.AmbientScope is not null; + public bool Enabled => _connectionStrings.IsConnectionStringConfigured() && + string.Equals(_connectionStrings.ProviderName, "Microsoft.Data.Sqlite", StringComparison.InvariantCultureIgnoreCase) && _efCoreScopeAccessor.Value.AmbientScope is not null; // With journal_mode=wal we can always read a snapshot. public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout; return new SqliteDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value); } // With journal_mode=wal only a single write transaction can exist at a time. public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout; return new SqliteDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value); } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs index 77975e8f31..a7f183e57a 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs @@ -17,8 +17,8 @@ namespace Umbraco.Cms.Persistence.SqlServer.Services; /// public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism { - private readonly IOptionsMonitor _connectionStrings; - private readonly IOptionsMonitor _globalSettings; + private ConnectionStrings _connectionStrings; + private GlobalSettings _globalSettings; private readonly ILogger _logger; private readonly Lazy _scopeAccessor; // Hooray it's a circular dependency. @@ -33,25 +33,28 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism { _logger = logger; _scopeAccessor = scopeAccessor; - _globalSettings = globalSettings; - _connectionStrings = connectionStrings; + _globalSettings = globalSettings.CurrentValue; + _connectionStrings = connectionStrings.CurrentValue; + globalSettings.OnChange(x => _globalSettings = x); + connectionStrings.OnChange(x => _connectionStrings = x); + } /// - public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && - string.Equals(_connectionStrings.CurrentValue.ProviderName,Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase); + public bool Enabled => _connectionStrings.IsConnectionStringConfigured() && + string.Equals(_connectionStrings.ProviderName,Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase); /// public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout; return new SqlServerDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value); } /// public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout; return new SqlServerDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value); } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs index 54e30d6fa6..f43a1eff05 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs @@ -16,8 +16,8 @@ namespace Umbraco.Cms.Persistence.Sqlite.Services; public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism { - private readonly IOptionsMonitor _connectionStrings; - private readonly IOptionsMonitor _globalSettings; + private ConnectionStrings _connectionStrings; + private GlobalSettings _globalSettings; private readonly ILogger _logger; private readonly Lazy _scopeAccessor; @@ -29,25 +29,27 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism { _logger = logger; _scopeAccessor = scopeAccessor; - _connectionStrings = connectionStrings; - _globalSettings = globalSettings; + _connectionStrings = connectionStrings.CurrentValue; + _globalSettings = globalSettings.CurrentValue; + globalSettings.OnChange(x=>_globalSettings = x); + connectionStrings.OnChange(x=>_connectionStrings = x); } /// - public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && - string.Equals(_connectionStrings.CurrentValue.ProviderName, Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase); + public bool Enabled => _connectionStrings.IsConnectionStringConfigured() && + string.Equals(_connectionStrings.ProviderName, Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase); // With journal_mode=wal we can always read a snapshot. public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout; return new SqliteDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value); } // With journal_mode=wal only a single write transaction can exist at a time. public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout; return new SqliteDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value); } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs index d1a8d10970..4e207702a0 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs @@ -194,6 +194,7 @@ public class ContentItemDisplay : /// This is not used for outgoing model information. /// [IgnoreDataMember] + [Obsolete("No longer used. Will be removed in V15.")] public ContentPropertyCollectionDto? ContentDto { get; set; } /// diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 7f839e81d1..e0f65cdd5c 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1,9 +1,12 @@ using System.Data.Common; using System.Globalization; using System.Linq.Expressions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; @@ -25,8 +28,11 @@ internal class UserService : RepositoryService, IUserService private readonly ILogger _logger; private readonly IRuntimeState _runtimeState; private readonly IUserGroupRepository _userGroupRepository; + private readonly IRequestCache _requestCache; private readonly IUserRepository _userRepository; + + [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")] public UserService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, @@ -35,11 +41,26 @@ internal class UserService : RepositoryService, IUserService IUserRepository userRepository, IUserGroupRepository userGroupRepository, IOptions globalSettings) + : this(provider, loggerFactory, eventMessagesFactory, runtimeState, userRepository, userGroupRepository, globalSettings, StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + public UserService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IRuntimeState runtimeState, + IUserRepository userRepository, + IUserGroupRepository userGroupRepository, + IOptions globalSettings, + IRequestCache requestCache) : base(provider, loggerFactory, eventMessagesFactory) { _runtimeState = runtimeState; _userRepository = userRepository; _userGroupRepository = userGroupRepository; + _requestCache = requestCache; _globalSettings = globalSettings.Value; _logger = loggerFactory.CreateLogger(); } @@ -1125,17 +1146,23 @@ internal class UserService : RepositoryService, IUserService /// Path to check permissions for public EntityPermissionSet GetPermissionsForPath(IUser? user, string? path) { - var nodeIds = path?.GetIdsFromPathReversed(); - - if (nodeIds is null || nodeIds.Length == 0 || user is null) + var result = (EntityPermissionSet?)_requestCache.Get($"{nameof(GetPermissionsForPath)}|{path}|{user?.Id}", () => { - return EntityPermissionSet.Empty(); - } + var nodeIds = path?.GetIdsFromPathReversed(); - // collect all permissions structures for all nodes for all groups belonging to the user - EntityPermission[] groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, true).ToArray(); + if (nodeIds is null || nodeIds.Length == 0 || user is null) + { + return EntityPermissionSet.Empty(); + } + + // collect all permissions structures for all nodes for all groups belonging to the user + EntityPermission[] groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, true).ToArray(); + + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + }); + + return result ?? EntityPermissionSet.Empty(); - return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); } /// diff --git a/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs b/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs new file mode 100644 index 0000000000..5cbf0e6dc2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs @@ -0,0 +1,32 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache.PropertyEditors; + +internal sealed class BlockEditorElementTypeCache : IBlockEditorElementTypeCache +{ + private readonly IContentTypeService _contentTypeService; + private readonly AppCaches _appCaches; + + public BlockEditorElementTypeCache(IContentTypeService contentTypeService, AppCaches appCaches) + { + _contentTypeService = contentTypeService; + _appCaches = appCaches; + } + + public IEnumerable GetAll(IEnumerable keys) + { + // TODO: make this less dumb; don't fetch all elements, only fetch the items that aren't yet in the cache and amend the cache as more elements are loaded + + const string cacheKey = $"{nameof(BlockEditorElementTypeCache)}_ElementTypes"; + IEnumerable? cachedElements = _appCaches.RequestCache.GetCacheItem>(cacheKey); + if (cachedElements is null) + { + cachedElements = _contentTypeService.GetAllElementTypes(); + _appCaches.RequestCache.Set(cacheKey, cachedElements); + } + + return cachedElements.Where(elementType => keys.Contains(elementType.Key)); + } +} diff --git a/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs b/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs new file mode 100644 index 0000000000..5ab1dc49af --- /dev/null +++ b/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Cache.PropertyEditors; + +public interface IBlockEditorElementTypeCache +{ + IEnumerable GetAll(IEnumerable keys); +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 10fbcc1207..77194cef2e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Options; using Serilog; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DeliveryApi; @@ -235,6 +236,8 @@ public static partial class UmbracoBuilderExtensions builder.AddDeliveryApiCoreServices(); builder.Services.AddTransient(); + builder.Services.AddSingleton(); + return builder; } diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs index 860c6199f7..98c28f92df 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs @@ -128,9 +128,9 @@ public class ContentValueSetBuilder : BaseValueSetBuilder, IContentVal // processing below instead of one by one. using (ICoreScope scope = _scopeProvider.CreateCoreScope()) { - creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) + creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).Distinct().ToArray()) .ToDictionary(x => x.Id, x => x); - writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) + writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).Distinct().ToArray()) .ToDictionary(x => x.Id, x => x); scope.Complete(); } diff --git a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs index e8226d994c..1bbc1d02cf 100644 --- a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; @@ -21,6 +22,7 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte private readonly IDeliveryApiContentIndexFieldDefinitionBuilder _deliveryApiContentIndexFieldDefinitionBuilder; private readonly IMemberService _memberService; private readonly IDeliveryApiCompositeIdHandler _deliveryApiCompositeIdHandler; + private readonly ICoreScopeProvider _coreScopeProvider; private DeliveryApiSettings _deliveryApiSettings; [Obsolete("Please use ctor that takes an IDeliveryApiCompositeIdHandler. Scheduled for removal in v15")] @@ -40,8 +42,33 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte deliveryApiContentIndexFieldDefinitionBuilder, deliveryApiSettings, memberService, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService() + ) { + } + [Obsolete("Please use ctor that takes an IDeliveryApiCompositeIdHandler. Scheduled for removal in v15")] + public DeliveryApiContentIndexValueSetBuilder( + ContentIndexHandlerCollection contentIndexHandlerCollection, + IContentService contentService, + IPublicAccessService publicAccessService, + ILogger logger, + IDeliveryApiContentIndexFieldDefinitionBuilder deliveryApiContentIndexFieldDefinitionBuilder, + IOptionsMonitor deliveryApiSettings, + IMemberService memberService, + IDeliveryApiCompositeIdHandler deliveryApiCompositeIdHandle) + :this( + contentIndexHandlerCollection, + contentService, + publicAccessService, + logger, + deliveryApiContentIndexFieldDefinitionBuilder, + deliveryApiSettings, + memberService, + deliveryApiCompositeIdHandle, + StaticServiceProvider.Instance.GetRequiredService()) + { + } public DeliveryApiContentIndexValueSetBuilder( @@ -52,7 +79,8 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte IDeliveryApiContentIndexFieldDefinitionBuilder deliveryApiContentIndexFieldDefinitionBuilder, IOptionsMonitor deliveryApiSettings, IMemberService memberService, - IDeliveryApiCompositeIdHandler deliveryApiCompositeIdHandler) + IDeliveryApiCompositeIdHandler deliveryApiCompositeIdHandler, + ICoreScopeProvider coreScopeProvider) { _contentIndexHandlerCollection = contentIndexHandlerCollection; _publicAccessService = publicAccessService; @@ -60,6 +88,7 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte _deliveryApiContentIndexFieldDefinitionBuilder = deliveryApiContentIndexFieldDefinitionBuilder; _memberService = memberService; _deliveryApiCompositeIdHandler = deliveryApiCompositeIdHandler; + _coreScopeProvider = coreScopeProvider; _contentService = contentService; _deliveryApiSettings = deliveryApiSettings.CurrentValue; deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings); @@ -68,6 +97,7 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte /// public IEnumerable GetValueSets(params IContent[] contents) { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); FieldDefinitionCollection fieldDefinitions = _deliveryApiContentIndexFieldDefinitionBuilder.Build(); foreach (IContent content in contents.Where(CanIndex)) { @@ -101,6 +131,8 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte yield return new ValueSet(_deliveryApiCompositeIdHandler.IndexId(content.Id, indexCulture), IndexTypes.Content, content.ContentType.Alias, indexValues); } + + scope.Complete(); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs index 8e17c6c477..fb58ab042d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Services; @@ -13,8 +14,8 @@ internal class BlockEditorValidator : BlockEditorValidatorBase public BlockEditorValidator( IPropertyValidationService propertyValidationService, BlockEditorValues blockEditorValues, - IContentTypeService contentTypeService) - : base(propertyValidationService, contentTypeService) + IBlockEditorElementTypeCache elementTypeCache) + : base(propertyValidationService, elementTypeCache) => _blockEditorValues = blockEditorValues; protected override IEnumerable GetElementTypeValidation(object? value) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs index 977d235229..4d131bc818 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs @@ -1,4 +1,5 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Cache.PropertyEditors; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Services; @@ -6,11 +7,11 @@ namespace Umbraco.Cms.Core.PropertyEditors; internal abstract class BlockEditorValidatorBase : ComplexEditorValidator { - private readonly IContentTypeService _contentTypeService; + private readonly IBlockEditorElementTypeCache _elementTypeCache; - protected BlockEditorValidatorBase(IPropertyValidationService propertyValidationService, IContentTypeService contentTypeService) + protected BlockEditorValidatorBase(IPropertyValidationService propertyValidationService, IBlockEditorElementTypeCache elementTypeCache) : base(propertyValidationService) - => _contentTypeService = contentTypeService; + => _elementTypeCache = elementTypeCache; protected IEnumerable GetBlockEditorDataValidation(BlockEditorData blockEditorData) { @@ -18,7 +19,7 @@ internal abstract class BlockEditorValidatorBase : ComplexEditorValidator // need to validate that data for each property especially for things like 'required' data to work. // Lookup all element types for all content/settings and then we can populate any empty properties. var allElements = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData).ToList(); - var allElementTypes = _contentTypeService.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); + var allElementTypes = _elementTypeCache.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); foreach (BlockItemData row in allElements) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs index 3270351838..98dbb85889 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs @@ -2,9 +2,9 @@ // See LICENSE for more details. using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; -using Umbraco.Cms.Core.Services; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; @@ -15,13 +15,13 @@ namespace Umbraco.Cms.Core.PropertyEditors; internal class BlockEditorValues { private readonly BlockEditorDataConverter _dataConverter; - private readonly IContentTypeService _contentTypeService; + private readonly IBlockEditorElementTypeCache _elementTypeCache; private readonly ILogger _logger; - public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger) + public BlockEditorValues(BlockEditorDataConverter dataConverter, IBlockEditorElementTypeCache elementTypeCache, ILogger logger) { _dataConverter = dataConverter; - _contentTypeService = contentTypeService; + _elementTypeCache = elementTypeCache; _logger = logger; } @@ -55,10 +55,14 @@ internal class BlockEditorValues var contentTypePropertyTypes = new Dictionary>(); // filter out any content that isn't referenced in the layout references + IEnumerable contentTypeKeys = blockEditorData.BlockValue.ContentData.Select(x => x.ContentTypeKey) + .Union(blockEditorData.BlockValue.SettingsData.Select(x => x.ContentTypeKey)).Distinct(); + 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))) { - ResolveBlockItemData(block, contentTypePropertyTypes); + ResolveBlockItemData(block, contentTypePropertyTypes, contentTypesDictionary); } // filter out any settings that isn't referenced in the layout references @@ -66,7 +70,7 @@ internal class BlockEditorValues blockEditorData.References.Any(r => r.SettingsUdi is not null && x.Udi is not null && r.SettingsUdi == x.Udi))) { - ResolveBlockItemData(block, contentTypePropertyTypes); + ResolveBlockItemData(block, contentTypePropertyTypes, contentTypesDictionary); } // remove blocks that couldn't be resolved @@ -76,12 +80,10 @@ internal class BlockEditorValues return blockEditorData; } - private IContentType? GetElementType(BlockItemData item) => _contentTypeService.Get(item.ContentTypeKey); - private bool ResolveBlockItemData(BlockItemData block, Dictionary> contentTypePropertyTypes) + private bool ResolveBlockItemData(BlockItemData block, Dictionary> contentTypePropertyTypes, IDictionary contentTypesDictionary) { - IContentType? contentType = GetElementType(block); - if (contentType == null) + if (contentTypesDictionary.TryGetValue(block.ContentTypeKey, out IContentType? contentType) is false) { return false; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs index fe72d83927..8d6e080b74 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs @@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; @@ -58,12 +59,12 @@ public abstract class BlockGridPropertyEditorBase : DataEditor IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - IContentTypeService contentTypeService, + IBlockEditorElementTypeCache elementTypeCache, IPropertyValidationService propertyValidationService) : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper) { - BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), contentTypeService, logger); - Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService)); + BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), elementTypeCache, logger); + Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache)); Validators.Add(new MinMaxValidator(BlockEditorValues, textService)); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index 5d2c968c72..690251467c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; @@ -55,7 +56,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor PropertyEditorCollection propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeConfigurationCache dataTypeConfigurationCache, - IContentTypeService contentTypeService, + IBlockEditorElementTypeCache elementTypeCache, ILocalizedTextService textService, ILogger logger, IShortStringHelper shortStringHelper, @@ -64,8 +65,8 @@ public abstract class BlockListPropertyEditorBase : DataEditor IPropertyValidationService propertyValidationService) : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper) { - BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, contentTypeService, logger); - Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService)); + BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, elementTypeCache, logger); + Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache)); Validators.Add(new MinMaxValidator(BlockEditorValues, textService)); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs index 01d10e46f0..dc4775f6b7 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -14,10 +15,10 @@ internal class RichTextEditorBlockValidator : BlockEditorValidatorBase public RichTextEditorBlockValidator( IPropertyValidationService propertyValidationService, BlockEditorValues blockEditorValues, - IContentTypeService contentTypeService, + IBlockEditorElementTypeCache elementTypeCache, IJsonSerializer jsonSerializer, ILogger logger) - : base(propertyValidationService, contentTypeService) + : base(propertyValidationService, elementTypeCache) { _blockEditorValues = blockEditorValues; _jsonSerializer = jsonSerializer; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 2447803277..ee37d8c63b 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; 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.Media; @@ -171,7 +172,7 @@ public class RichTextPropertyEditor : DataEditor private readonly IHtmlMacroParameterParser _macroParameterParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly IJsonSerializer _jsonSerializer; - private readonly IContentTypeService _contentTypeService; + private readonly IBlockEditorElementTypeCache _elementTypeCache; private readonly ILogger _logger; public RichTextPropertyValueEditor( @@ -189,7 +190,7 @@ public class RichTextPropertyEditor : DataEditor IIOHelper ioHelper, IHtmlSanitizer htmlSanitizer, IHtmlMacroParameterParser macroParameterParser, - IContentTypeService contentTypeService, + IBlockEditorElementTypeCache elementTypeCache, IPropertyValidationService propertyValidationService, DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection) : base(attribute, propertyEditors, dataTypeReadCache, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection) @@ -200,11 +201,11 @@ public class RichTextPropertyEditor : DataEditor _pastedImages = pastedImages; _htmlSanitizer = htmlSanitizer; _macroParameterParser = macroParameterParser; - _contentTypeService = contentTypeService; + _elementTypeCache = elementTypeCache; _jsonSerializer = jsonSerializer; _logger = logger; - Validators.Add(new RichTextEditorBlockValidator(propertyValidationService, CreateBlockEditorValues(), contentTypeService, jsonSerializer, logger)); + Validators.Add(new RichTextEditorBlockValidator(propertyValidationService, CreateBlockEditorValues(), elementTypeCache, jsonSerializer, logger)); } /// @@ -392,6 +393,6 @@ public class RichTextPropertyEditor : DataEditor } private BlockEditorValues CreateBlockEditorValues() - => new(new RichTextEditorBlockDataConverter(), _contentTypeService, _logger); + => new(new RichTextEditorBlockDataConverter(), _elementTypeCache, _logger); } } diff --git a/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs b/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs index 6c7445d2da..bb8c4d0d86 100644 --- a/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs +++ b/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs @@ -55,10 +55,9 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser macroAlias, new Dictionary(macroAttributes, StringComparer.OrdinalIgnoreCase)))); - foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacros(foundMacros)) - { - yield return umbracoEntityReference; - } + return foundMacros.Count > 0 + ? GetUmbracoEntityReferencesFromMacros(foundMacros) + : Enumerable.Empty(); } /// @@ -82,10 +81,9 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser } } - foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacros(foundMacros)) - { - yield return umbracoEntityReference; - } + return foundMacros.Count > 0 + ? GetUmbracoEntityReferencesFromMacros(foundMacros) + : Enumerable.Empty(); } private IEnumerable GetUmbracoEntityReferencesFromMacros( @@ -96,6 +94,7 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser yield break; } + IEnumerable uniqueMacroAliases = macros.Select(f => f.Item1).Distinct(); // TODO: Tracking Macro references @@ -103,7 +102,9 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser var foundMacroUmbracoEntityReferences = new List(); // Get all the macro configs in one hit for these unique macro aliases - this is now cached with a custom cache policy - IEnumerable macroConfigs = macroWithAliasService.GetAll(uniqueMacroAliases.WhereNotNull().ToArray()); + IEnumerable macroConfigs = uniqueMacroAliases.Any() + ? macroWithAliasService.GetAll(uniqueMacroAliases.WhereNotNull().ToArray()) + : Enumerable.Empty(); foreach (Tuple> macro in macros) { diff --git a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs index 0bc4e9c1b7..ae48169943 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs @@ -128,7 +128,7 @@ internal class ContentMapDefinition : IMapDefinition target.AdditionalPreviewUrls = source.AdditionalPreviewUrls; } - // Umbraco.Code.MapAll + // Umbraco.Code.MapAll -ContentDto private void Map(ContentItemDisplay source, ContentItemDisplayWithSchedule target, MapperContext context) { foreach (KeyValuePair additionalData in source.AdditionalData) @@ -140,7 +140,6 @@ internal class ContentMapDefinition : IMapDefinition target.AllowedTemplates = source.AllowedTemplates; target.AllowPreview = source.AllowPreview; target.ContentApps = source.ContentApps; - target.ContentDto = source.ContentDto; target.ContentTypeAlias = source.ContentTypeAlias; target.ContentTypeId = source.ContentTypeId; target.ContentTypeKey = source.ContentTypeKey; @@ -207,7 +206,7 @@ internal class ContentMapDefinition : IMapDefinition } } - // Umbraco.Code.MapAll + // Umbraco.Code.MapAll -ContentDto private static void Map(ContentItemDisplayWithSchedule source, ContentItemDisplay target, MapperContext context) { foreach (KeyValuePair additionalData in source.AdditionalData) @@ -219,7 +218,6 @@ internal class ContentMapDefinition : IMapDefinition target.AllowedTemplates = source.AllowedTemplates; target.AllowPreview = source.AllowPreview; target.ContentApps = source.ContentApps; - target.ContentDto = source.ContentDto; target.ContentTypeAlias = source.ContentTypeAlias; target.ContentTypeId = source.ContentTypeId; target.ContentTypeKey = source.ContentTypeKey; @@ -253,7 +251,7 @@ internal class ContentMapDefinition : IMapDefinition private static void Map(IContent source, ContentPropertyCollectionDto target, MapperContext context) => target.Properties = context.MapEnumerable(source.Properties).WhereNotNull(); - // Umbraco.Code.MapAll -AllowPreview -Errors -PersistedContent + // Umbraco.Code.MapAll -AllowPreview -Errors -PersistedContent -ContentDto private void Map(IContent source, ContentItemDisplay target, MapperContext context) where TVariant : ContentVariantDisplay { @@ -300,11 +298,6 @@ internal class ContentMapDefinition : IMapDefinition target.Updater = _commonMapper.GetCreator(source, context); target.Urls = GetUrls(source); target.Variants = _contentVariantMapper.Map(source, context); - - target.ContentDto = new ContentPropertyCollectionDto - { - Properties = context.MapEnumerable(source.Properties).WhereNotNull() - }; } // Umbraco.Code.MapAll -Segment -Language -DisplayName -AdditionalPreviewUrls diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs index 0874b85987..b04acdbbc4 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.PropertyEditors; @@ -44,7 +45,7 @@ public class DataValueEditorReuseTests _propertyEditorCollection, _dataValueReferenceFactories, Mock.Of(), - Mock.Of(), + Mock.Of(), Mock.Of(), Mock.Of>(), Mock.Of(),