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 <kja@umbraco.dk>
This commit is contained in:
Bjarke Berg
2024-06-03 11:23:25 +02:00
committed by GitHub
parent 528583430f
commit 0aaac78cfa
21 changed files with 210 additions and 97 deletions

View File

@@ -16,8 +16,8 @@ namespace Umbraco.Cms.Persistence.EFCore.Locking;
internal class SqlServerEFCoreDistributedLockingMechanism<T> : IDistributedLockingMechanism
where T : DbContext
{
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
private ConnectionStrings _connectionStrings;
private GlobalSettings _globalSettings;
private readonly ILogger<SqlServerEFCoreDistributedLockingMechanism<T>> _logger;
private readonly Lazy<IEFCoreScopeAccessor<T>> _scopeAccessor; // Hooray it's a circular dependency.
@@ -32,27 +32,29 @@ internal class SqlServerEFCoreDistributedLockingMechanism<T> : 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;
/// <inheritdoc />
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;
/// <inheritdoc />
public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null)
{
obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout;
obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout;
return new SqlServerDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value);
}
/// <inheritdoc />
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<T> : 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)
{

View File

@@ -16,8 +16,8 @@ namespace Umbraco.Cms.Persistence.EFCore.Locking;
internal class SqliteEFCoreDistributedLockingMechanism<T> : IDistributedLockingMechanism
where T : DbContext
{
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
private ConnectionStrings _connectionStrings;
private GlobalSettings _globalSettings;
private readonly ILogger<SqliteEFCoreDistributedLockingMechanism<T>> _logger;
private readonly Lazy<IEFCoreScopeAccessor<T>> _efCoreScopeAccessor;
@@ -29,27 +29,29 @@ internal class SqliteEFCoreDistributedLockingMechanism<T> : 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;
/// <inheritdoc />
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);
}

View File

@@ -17,8 +17,8 @@ namespace Umbraco.Cms.Persistence.SqlServer.Services;
/// </summary>
public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism
{
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
private ConnectionStrings _connectionStrings;
private GlobalSettings _globalSettings;
private readonly ILogger<SqlServerDistributedLockingMechanism> _logger;
private readonly Lazy<IScopeAccessor> _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);
}
/// <inheritdoc />
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);
/// <inheritdoc />
public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null)
{
obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout;
obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout;
return new SqlServerDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value);
}
/// <inheritdoc />
public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null)
{
obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout;
obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout;
return new SqlServerDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value);
}

View File

@@ -16,8 +16,8 @@ namespace Umbraco.Cms.Persistence.Sqlite.Services;
public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism
{
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
private ConnectionStrings _connectionStrings;
private GlobalSettings _globalSettings;
private readonly ILogger<SqliteDistributedLockingMechanism> _logger;
private readonly Lazy<IScopeAccessor> _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);
}
/// <inheritdoc />
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);
}

View File

@@ -194,6 +194,7 @@ public class ContentItemDisplay<TVariant> :
/// This is not used for outgoing model information.
/// </remarks>
[IgnoreDataMember]
[Obsolete("No longer used. Will be removed in V15.")]
public ContentPropertyCollectionDto? ContentDto { get; set; }
/// <summary>

View File

@@ -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<UserService> _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> globalSettings)
: this(provider, loggerFactory, eventMessagesFactory, runtimeState, userRepository, userGroupRepository, globalSettings, StaticServiceProvider.Instance.GetRequiredService<IRequestCache>())
{
}
public UserService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IRuntimeState runtimeState,
IUserRepository userRepository,
IUserGroupRepository userGroupRepository,
IOptions<GlobalSettings> globalSettings,
IRequestCache requestCache)
: base(provider, loggerFactory, eventMessagesFactory)
{
_runtimeState = runtimeState;
_userRepository = userRepository;
_userGroupRepository = userGroupRepository;
_requestCache = requestCache;
_globalSettings = globalSettings.Value;
_logger = loggerFactory.CreateLogger<UserService>();
}
@@ -1125,17 +1146,23 @@ internal class UserService : RepositoryService, IUserService
/// <param name="path">Path to check permissions for</param>
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);
}
/// <summary>

View File

@@ -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<IContentType> GetAll(IEnumerable<Guid> 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<IContentType>? cachedElements = _appCaches.RequestCache.GetCacheItem<IEnumerable<IContentType>>(cacheKey);
if (cachedElements is null)
{
cachedElements = _contentTypeService.GetAllElementTypes();
_appCaches.RequestCache.Set(cacheKey, cachedElements);
}
return cachedElements.Where(elementType => keys.Contains(elementType.Key));
}
}

View File

@@ -0,0 +1,8 @@
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Cache.PropertyEditors;
public interface IBlockEditorElementTypeCache
{
IEnumerable<IContentType> GetAll(IEnumerable<Guid> keys);
}

View File

@@ -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<IWebhookFiringService, WebhookFiringService>();
builder.Services.AddSingleton<IBlockEditorElementTypeCache, BlockEditorElementTypeCache>();
return builder;
}

View File

@@ -128,9 +128,9 @@ public class ContentValueSetBuilder : BaseValueSetBuilder<IContent>, 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();
}

View File

@@ -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<IDeliveryApiCompositeIdHandler>())
StaticServiceProvider.Instance.GetRequiredService<IDeliveryApiCompositeIdHandler>(),
StaticServiceProvider.Instance.GetRequiredService<ICoreScopeProvider>()
)
{
}
[Obsolete("Please use ctor that takes an IDeliveryApiCompositeIdHandler. Scheduled for removal in v15")]
public DeliveryApiContentIndexValueSetBuilder(
ContentIndexHandlerCollection contentIndexHandlerCollection,
IContentService contentService,
IPublicAccessService publicAccessService,
ILogger<DeliveryApiContentIndexValueSetBuilder> logger,
IDeliveryApiContentIndexFieldDefinitionBuilder deliveryApiContentIndexFieldDefinitionBuilder,
IOptionsMonitor<DeliveryApiSettings> deliveryApiSettings,
IMemberService memberService,
IDeliveryApiCompositeIdHandler deliveryApiCompositeIdHandle)
:this(
contentIndexHandlerCollection,
contentService,
publicAccessService,
logger,
deliveryApiContentIndexFieldDefinitionBuilder,
deliveryApiSettings,
memberService,
deliveryApiCompositeIdHandle,
StaticServiceProvider.Instance.GetRequiredService<ICoreScopeProvider>())
{
}
public DeliveryApiContentIndexValueSetBuilder(
@@ -52,7 +79,8 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte
IDeliveryApiContentIndexFieldDefinitionBuilder deliveryApiContentIndexFieldDefinitionBuilder,
IOptionsMonitor<DeliveryApiSettings> 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
/// <inheritdoc />
public IEnumerable<ValueSet> 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();
}
}

View File

@@ -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<ElementTypeValidationModel> GetElementTypeValidation(object? value)

View File

@@ -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<ElementTypeValidationModel> 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)
{

View File

@@ -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<string, Dictionary<string, IPropertyType>>();
// filter out any content that isn't referenced in the layout references
IEnumerable<Guid> contentTypeKeys = blockEditorData.BlockValue.ContentData.Select(x => x.ContentTypeKey)
.Union(blockEditorData.BlockValue.SettingsData.Select(x => x.ContentTypeKey)).Distinct();
IDictionary<Guid, IContentType> 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<string, Dictionary<string, IPropertyType>> contentTypePropertyTypes)
private bool ResolveBlockItemData(BlockItemData block, Dictionary<string, Dictionary<string, IPropertyType>> contentTypePropertyTypes, IDictionary<Guid, IContentType> contentTypesDictionary)
{
IContentType? contentType = GetElementType(block);
if (contentType == null)
if (contentTypesDictionary.TryGetValue(block.ContentTypeKey, out IContentType? contentType) is false)
{
return false;
}

View File

@@ -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));
}

View File

@@ -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<BlockEditorPropertyValueEditor> 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));
}

View File

@@ -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;

View File

@@ -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<RichTextPropertyValueEditor> _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));
}
/// <inheritdoc />
@@ -392,6 +393,6 @@ public class RichTextPropertyEditor : DataEditor
}
private BlockEditorValues CreateBlockEditorValues()
=> new(new RichTextEditorBlockDataConverter(), _contentTypeService, _logger);
=> new(new RichTextEditorBlockDataConverter(), _elementTypeCache, _logger);
}
}

View File

@@ -55,10 +55,9 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
macroAlias,
new Dictionary<string, string>(macroAttributes, StringComparer.OrdinalIgnoreCase))));
foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacros(foundMacros))
{
yield return umbracoEntityReference;
}
return foundMacros.Count > 0
? GetUmbracoEntityReferencesFromMacros(foundMacros)
: Enumerable.Empty<UmbracoEntityReference>();
}
/// <summary>
@@ -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<UmbracoEntityReference>();
}
private IEnumerable<UmbracoEntityReference> GetUmbracoEntityReferencesFromMacros(
@@ -96,6 +94,7 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
yield break;
}
IEnumerable<string?> uniqueMacroAliases = macros.Select(f => f.Item1).Distinct();
// TODO: Tracking Macro references
@@ -103,7 +102,9 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
var foundMacroUmbracoEntityReferences = new List<UmbracoEntityReference>();
// Get all the macro configs in one hit for these unique macro aliases - this is now cached with a custom cache policy
IEnumerable<IMacro> macroConfigs = macroWithAliasService.GetAll(uniqueMacroAliases.WhereNotNull().ToArray());
IEnumerable<IMacro> macroConfigs = uniqueMacroAliases.Any()
? macroWithAliasService.GetAll(uniqueMacroAliases.WhereNotNull().ToArray())
: Enumerable.Empty<IMacro>();
foreach (Tuple<string?, Dictionary<string, string>> macro in macros)
{

View File

@@ -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<string, object> 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<string, object> 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<IProperty, ContentPropertyDto>(source.Properties).WhereNotNull();
// Umbraco.Code.MapAll -AllowPreview -Errors -PersistedContent
// Umbraco.Code.MapAll -AllowPreview -Errors -PersistedContent -ContentDto
private void Map<TVariant>(IContent source, ContentItemDisplay<TVariant> 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<TVariant>(source, context);
target.ContentDto = new ContentPropertyCollectionDto
{
Properties = context.MapEnumerable<IProperty, ContentPropertyDto>(source.Properties).WhereNotNull()
};
}
// Umbraco.Code.MapAll -Segment -Language -DisplayName -AdditionalPreviewUrls

View File

@@ -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<IDataTypeConfigurationCache>(),
Mock.Of<IContentTypeService>(),
Mock.Of<IBlockEditorElementTypeCache>(),
Mock.Of<ILocalizedTextService>(),
Mock.Of<ILogger<BlockListPropertyEditorBase.BlockListEditorPropertyValueEditor>>(),
Mock.Of<IShortStringHelper>(),