diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs index 1fe1e92d9b..5969e0a788 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs @@ -113,7 +113,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService if (parts.Length != 2) { // invalid filter - _logger.LogInformation($"The \"{nameof(filters)}\" query option \"{filter}\" is not valid"); + _logger.LogInformation("An invalid filter option was encountered. Please ensure that supplied filter options are two-part, separated by ':'."); return null; } @@ -127,7 +127,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService break; default: // unknown filter - _logger.LogInformation($"The \"{nameof(filters)}\" query option \"{filter}\" is not supported"); + _logger.LogInformation("An unsupported filter option was supplied for the query. Please use only valid filter options. See the documentation for details."); return null; } } @@ -143,7 +143,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService if (parts.Length != 2) { // invalid sort - _logger.LogInformation($"The \"{nameof(sorts)}\" query option \"{sort}\" is not valid"); + _logger.LogInformation("An invalid sort option was encountered. Please ensure that the supplied sort options are two-part, separated by ':'."); return null; } @@ -164,7 +164,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService break; default: // unknown sort - _logger.LogInformation($"The \"{nameof(sorts)}\" query option \"{sort}\" is not supported"); + _logger.LogInformation("An unsupported sort option was supplied for the query. Please use only valid sort options. See the documentation for details."); return null; } diff --git a/src/Umbraco.Core/Composing/BuilderCollectionBase.cs b/src/Umbraco.Core/Composing/BuilderCollectionBase.cs index 722a6efca0..e92790ab4e 100644 --- a/src/Umbraco.Core/Composing/BuilderCollectionBase.cs +++ b/src/Umbraco.Core/Composing/BuilderCollectionBase.cs @@ -3,7 +3,7 @@ using System.Collections; namespace Umbraco.Cms.Core.Composing; /// -/// Provides a base class for builder collections. +/// Provides a base class for builder collections. /// /// The type of the items. public abstract class BuilderCollectionBase : IBuilderCollection @@ -11,21 +11,18 @@ public abstract class BuilderCollectionBase : IBuilderCollection private readonly LazyReadOnlyCollection _items; /// - /// Initializes a new instance of the with items. + /// Initializes a new instance of the with items. /// /// The items. - public BuilderCollectionBase(Func> items) => _items = new LazyReadOnlyCollection(items); + public BuilderCollectionBase(Func> items) + => _items = new LazyReadOnlyCollection(items); /// public int Count => _items.Count; - /// - /// Gets an enumerator. - /// + /// public IEnumerator GetEnumerator() => _items.GetEnumerator(); - /// - /// Gets an enumerator. - /// + /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Umbraco.Core/Composing/IBuilderCollection.cs b/src/Umbraco.Core/Composing/IBuilderCollection.cs index 56036997bc..c8920149c5 100644 --- a/src/Umbraco.Core/Composing/IBuilderCollection.cs +++ b/src/Umbraco.Core/Composing/IBuilderCollection.cs @@ -1,13 +1,16 @@ namespace Umbraco.Cms.Core.Composing; /// -/// Represents a builder collection, ie an immutable enumeration of items. +/// Represents a builder collection, ie an immutable enumeration of items. /// /// The type of the items. public interface IBuilderCollection : IEnumerable { /// - /// Gets the number of items in the collection. + /// Gets the number of items in the collection. /// + /// + /// The count. + /// int Count { get; } } diff --git a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs index c093962408..3f02be10b1 100644 --- a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs +++ b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs @@ -1,49 +1,88 @@ namespace Umbraco.Cms.Core.Models.Editors; /// -/// Used to track reference to other entities in a property value +/// Used to track a reference to another entity in a property value. /// public struct UmbracoEntityReference : IEquatable { private static readonly UmbracoEntityReference _empty = new(UnknownTypeUdi.Instance, string.Empty); + /// + /// Initializes a new instance of the struct. + /// + /// The UDI. + /// The relation type alias. public UmbracoEntityReference(Udi udi, string relationTypeAlias) { Udi = udi ?? throw new ArgumentNullException(nameof(udi)); RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias)); } + /// + /// Initializes a new instance of the struct for a document or media item. + /// + /// The UDI. public UmbracoEntityReference(Udi udi) { Udi = udi ?? throw new ArgumentNullException(nameof(udi)); switch (udi.EntityType) { + case Constants.UdiEntityType.Document: + RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias; + break; case Constants.UdiEntityType.Media: RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias; break; default: - RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias; + // No relation type alias convention for this entity type, so leave it empty + RelationTypeAlias = string.Empty; break; } } + /// + /// Gets the UDI. + /// + /// + /// The UDI. + /// public Udi Udi { get; } - public static UmbracoEntityReference Empty() => _empty; - - public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty(); - + /// + /// Gets the relation type alias. + /// + /// + /// The relation type alias. + /// public string RelationTypeAlias { get; } - public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right); + /// + /// Gets an empty reference. + /// + /// + /// An empty reference. + /// + public static UmbracoEntityReference Empty() => _empty; + /// + /// Determines whether the specified reference is empty. + /// + /// The reference. + /// + /// true if the specified reference is empty; otherwise, false. + /// + public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty(); + + /// public override bool Equals(object? obj) => obj is UmbracoEntityReference reference && Equals(reference); + /// public bool Equals(UmbracoEntityReference other) => EqualityComparer.Default.Equals(Udi, other.Udi) && RelationTypeAlias == other.RelationTypeAlias; + /// public override int GetHashCode() { var hashCode = -487348478; @@ -52,5 +91,9 @@ public struct UmbracoEntityReference : IEquatable return hashCode; } + /// + public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right); + + /// public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right) => !(left == right); } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index c605d45a9a..f5eb0130fc 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -4,56 +4,107 @@ using Umbraco.Cms.Core.Models.Editors; namespace Umbraco.Cms.Core.PropertyEditors; +/// +/// Provides a builder collection for items. +/// public class DataValueReferenceFactoryCollection : BuilderCollectionBase { + // TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented + // by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection + + /// + /// Initializes a new instance of the class. + /// + /// The items. public DataValueReferenceFactoryCollection(Func> items) : base(items) { } - // TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented - // by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection + /// + /// Gets all unique references from the specified properties. + /// + /// The properties. + /// The property editors. + /// + /// The unique references from the specified properties. + /// public ISet GetAllReferences(IPropertyCollection properties, PropertyEditorCollection propertyEditors) { var references = new HashSet(); - foreach (IProperty property in properties) + // Group by property editor alias to avoid duplicate lookups and optimize value parsing + foreach (var propertyValuesByPropertyEditorAlias in properties.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Values)) { - if (!propertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor)) + if (!propertyEditors.TryGet(propertyValuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor)) { continue; } - // TODO: We will need to change this once we support tracking via variants/segments - // for now, we are tracking values from ALL variants - foreach (IPropertyValue propertyValue in property.Values) + // Use distinct values to avoid duplicate parsing of the same value + var values = new HashSet(properties.Count); + foreach (IPropertyValue propertyValue in propertyValuesByPropertyEditorAlias.SelectMany(x => x)) { - object? value = propertyValue.EditedValue; - - if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference) - { - references.UnionWith(dataValueReference.GetReferences(value)); - } - - // Loop over collection that may be add to existing property editors - // implementation of GetReferences in IDataValueReference. - // Allows developers to add support for references by a - // package /property editor that did not implement IDataValueReference themselves - foreach (IDataValueReferenceFactory dataValueReferenceFactory in this) - { - // Check if this value reference is for this datatype/editor - // Then call it's GetReferences method - to see if the value stored - // in the dataeditor/property has references to media/content items - if (dataValueReferenceFactory.IsForEditor(dataEditor)) - { - references.UnionWith(dataValueReferenceFactory.GetDataValueReference().GetReferences(value)); - } - } + values.Add(propertyValue.EditedValue); + values.Add(propertyValue.PublishedValue); } + + references.UnionWith(GetReferences(dataEditor, values)); } return references; } + /// + /// Gets the references. + /// + /// The data editor. + /// The values. + /// + /// The references. + /// + public IEnumerable GetReferences(IDataEditor dataEditor, params object?[] values) + => GetReferences(dataEditor, (IEnumerable)values); + + /// + /// Gets the references. + /// + /// The data editor. + /// The values. + /// + /// The references. + /// + public IEnumerable GetReferences(IDataEditor dataEditor, IEnumerable values) + { + // TODO: We will need to change this once we support tracking via variants/segments + // for now, we are tracking values from ALL variants + if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference) + { + foreach (UmbracoEntityReference reference in values.SelectMany(dataValueReference.GetReferences)) + { + yield return reference; + } + } + + // Loop over collection that may be add to existing property editors + // implementation of GetReferences in IDataValueReference. + // Allows developers to add support for references by a + // package /property editor that did not implement IDataValueReference themselves + foreach (IDataValueReferenceFactory dataValueReferenceFactory in this) + { + // Check if this value reference is for this datatype/editor + // Then call it's GetReferences method - to see if the value stored + // in the dataeditor/property has references to media/content items + if (dataValueReferenceFactory.IsForEditor(dataEditor)) + { + IDataValueReference factoryDataValueReference = dataValueReferenceFactory.GetDataValueReference(); + foreach (UmbracoEntityReference reference in values.SelectMany(factoryDataValueReference.GetReferences)) + { + yield return reference; + } + } + } + } + /// /// Gets all relation type aliases that are automatically tracked. /// @@ -61,7 +112,7 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase /// All relation type aliases that are automatically tracked. /// - public ISet GetAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors) + public ISet GetAllAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors) { // Always add default automatic relation types var automaticRelationTypeAliases = new HashSet(Constants.Conventions.RelationTypes.AutomaticRelationTypes); @@ -76,31 +127,13 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase - /// Gets the relation type aliases that are automatically tracked for all properties. + /// Gets the automatic relation types aliases. /// - /// The properties. - /// The property editors. + /// The data editor. /// - /// The relation type aliases that are automatically tracked for all properties. + /// The automatic relation types aliases. /// - public ISet GetAutomaticRelationTypesAliases(IPropertyCollection properties, PropertyEditorCollection propertyEditors) - { - // Always add default automatic relation types - var automaticRelationTypeAliases = new HashSet(Constants.Conventions.RelationTypes.AutomaticRelationTypes); - - // Only add relation types that are used in the properties - foreach (IProperty property in properties) - { - if (propertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor)) - { - automaticRelationTypeAliases.UnionWith(GetAutomaticRelationTypesAliases(dataEditor)); - } - } - - return automaticRelationTypeAliases; - } - - private IEnumerable GetAutomaticRelationTypesAliases(IDataEditor dataEditor) + public IEnumerable GetAutomaticRelationTypesAliases(IDataEditor dataEditor) { if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference) { diff --git a/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs index c20ebf9284..2d4345c053 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs @@ -1,6 +1,8 @@ using System.Xml.XPath; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Xml; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PublishedCache; @@ -9,10 +11,24 @@ public abstract class PublishedCacheBase : IPublishedCache { private readonly IVariationContextAccessor? _variationContextAccessor; - public PublishedCacheBase(IVariationContextAccessor variationContextAccessor) => _variationContextAccessor = - variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); - protected PublishedCacheBase(bool previewDefault) => PreviewDefault = previewDefault; + [Obsolete("Use ctor with all parameters. This will be removed in V15")] + public PublishedCacheBase(IVariationContextAccessor variationContextAccessor) + : this(variationContextAccessor, false) + { + } + + [Obsolete("Use ctor with all parameters. This will be removed in V15")] + protected PublishedCacheBase(bool previewDefault) + : this(StaticServiceProvider.Instance.GetRequiredService(), previewDefault) + { + } + + public PublishedCacheBase(IVariationContextAccessor variationContextAccessor, bool previewDefault) + { + _variationContextAccessor = variationContextAccessor; + PreviewDefault = previewDefault; + } public bool PreviewDefault { get; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 6df4d66ab5..d917f90f33 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -1082,20 +1082,24 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected void PersistRelations(TEntity entity) { - // Get all references from our core built in DataEditors/Property Editors - // Along with seeing if developers want to collect additional references from the DataValueReferenceFactories collection + // Get all references and automatic relation type aliases ISet references = _dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors); - - // First delete all auto-relations for this entity - ISet automaticRelationTypeAliases = _dataValueReferenceFactories.GetAutomaticRelationTypesAliases(entity.Properties, PropertyEditors); - RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray()); + ISet automaticRelationTypeAliases = _dataValueReferenceFactories.GetAllAutomaticRelationTypesAliases(PropertyEditors); if (references.Count == 0) { + // Delete all relations using the automatic relation type aliases + RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray()); + // No need to add new references/relations return; } + // Lookup all relation type IDs + var relationTypeLookup = RelationTypeRepository.GetMany(Array.Empty()) + .Where(x => automaticRelationTypeAliases.Contains(x.Alias)) + .ToDictionary(x => x.Alias, x => x.Id); + // Lookup node IDs for all GUID based UDIs IEnumerable keys = references.Select(x => x.Udi).OfType().Select(x => x.Guid); var keysLookup = Database.FetchByGroups(keys, Constants.Sql.MaxParameterCount, guids => @@ -1106,15 +1110,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement .WhereIn(x => x.UniqueId, guids); }).ToDictionary(x => x.UniqueId, x => x.NodeId); - // Lookup all relation type IDs - var relationTypeLookup = RelationTypeRepository.GetMany(Array.Empty()).ToDictionary(x => x.Alias, x => x.Id); - // Get all valid relations - var relations = new List(references.Count); + var relations = new List<(int ChildId, int RelationTypeId)>(references.Count); foreach (UmbracoEntityReference reference in references) { - if (!automaticRelationTypeAliases.Contains(reference.RelationTypeAlias)) + if (string.IsNullOrEmpty(reference.RelationTypeAlias)) { + // Reference does not specify a relation type alias, so skip adding a relation + Logger.LogDebug("The reference to {Udi} does not specify a relation type alias, so it will not be saved as relation.", reference.Udi); + } + else if (!automaticRelationTypeAliases.Contains(reference.RelationTypeAlias)) + { // Returning a reference that doesn't use an automatic relation type is an issue that should be fixed in code Logger.LogError("The reference to {Udi} uses a relation type {RelationTypeAlias} that is not an automatic relation type.", reference.Udi, reference.RelationTypeAlias); } @@ -1130,12 +1136,24 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } else { - relations.Add(new ReadOnlyRelation(entity.Id, id, relationTypeId)); + relations.Add((id, relationTypeId)); } } - // Save bulk relations - RelationRepository.SaveBulk(relations); + // Get all existing relations (optimize for adding new and keeping existing relations) + var query = Query().Where(x => x.ParentId == entity.Id).WhereIn(x => x.RelationTypeId, relationTypeLookup.Values); + var existingRelations = RelationRepository.GetPagedRelationsByQuery(query, 0, int.MaxValue, out _, null) + .ToDictionary(x => (x.ChildId, x.RelationTypeId)); // Relations are unique by parent ID, child ID and relation type ID + + // Add relations that don't exist yet + var relationsToAdd = relations.Except(existingRelations.Keys).Select(x => new ReadOnlyRelation(entity.Id, x.ChildId, x.RelationTypeId)); + RelationRepository.SaveBulk(relationsToAdd); + + // Delete relations that don't exist anymore + foreach (IRelation relation in existingRelations.Where(x => !relations.Contains(x.Key)).Select(x => x.Value)) + { + RelationRepository.Delete(relation); + } } /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index b8d9a6c468..b4bd57ce33 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -16,18 +16,27 @@ namespace Umbraco.Cms.Core.PropertyEditors; internal abstract class BlockEditorPropertyValueEditor : BlockValuePropertyValueEditorBase { private BlockEditorValues? _blockEditorValues; + private readonly IDataTypeService _dataTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories; + private readonly ILogger _logger; protected BlockEditorPropertyValueEditor( DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeService dataTypeService, ILocalizedTextService textService, ILogger logger, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper) - : base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper) + : base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactories) { + _propertyEditors = propertyEditors; + _dataValueReferenceFactories = dataValueReferenceFactories; + _dataTypeService = dataTypeService; + _logger = logger; } protected BlockEditorValues BlockEditorValues @@ -39,30 +48,57 @@ internal abstract class BlockEditorPropertyValueEditor : BlockValuePropertyValue /// public override IEnumerable GetReferences(object? value) { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var result = new List(); - BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson); - if (blockEditorData == null) + // 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)) { - return Enumerable.Empty(); - } + if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor)) + { + continue; + } - return GetBlockValueReferences(blockEditorData.BlockValue); + // Use distinct values to avoid duplicate parsing of the same value + foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct())) + { + yield return reference; + } + } } /// 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 = _dataTypeService.GetDataType(propertyValue.PropertyType.DataTypeKey)?.Configuration; + foreach (ITag tag in dataValueTags.GetTags(propertyValue.Value, configuration, languageId)) + { + yield return tag; + } + } + } + + private IEnumerable GetAllPropertyValues(object? value) { var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson); - if (blockEditorData == null) + if (blockEditorData is null) { - return Enumerable.Empty(); + yield break; } - return GetBlockValueTags(blockEditorData.BlockValue, languageId); + // 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; + } } // note: there is NO variant support here diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs index 73b767bd4f..7e67c2d8a7 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs @@ -50,6 +50,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor public BlockGridEditorPropertyValueEditor( DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeService dataTypeService, ILocalizedTextService textService, ILogger logger, @@ -58,7 +59,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor IIOHelper ioHelper, IContentTypeService contentTypeService, IPropertyValidationService propertyValidationService) - : base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper) + : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper) { BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), contentTypeService, logger); Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService)); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index a2f5616518..3d39ba6eb5 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -52,6 +52,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor DataEditorAttribute attribute, BlockEditorDataConverter blockEditorDataConverter, PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService, @@ -60,7 +61,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor IJsonSerializer jsonSerializer, IIOHelper ioHelper, IPropertyValidationService propertyValidationService) : - base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper) + base(attribute, propertyEditors, dataValueReferenceFactories,dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper) { BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, contentTypeService, logger); Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService)); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs index d83d71abaa..c908a3cc81 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs @@ -14,6 +14,7 @@ internal abstract class BlockValuePropertyValueEditorBase : DataValueEditor, IDa private readonly IDataTypeService _dataTypeService; private readonly PropertyEditorCollection _propertyEditors; private readonly ILogger _logger; + private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactoryCollection; protected BlockValuePropertyValueEditorBase( DataEditorAttribute attribute, @@ -23,12 +24,14 @@ internal abstract class BlockValuePropertyValueEditorBase : DataValueEditor, IDa ILogger logger, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, - IIOHelper ioHelper) + IIOHelper ioHelper, + DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection) : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; _logger = logger; + _dataValueReferenceFactoryCollection = dataValueReferenceFactoryCollection; } /// @@ -36,30 +39,33 @@ internal abstract class BlockValuePropertyValueEditorBase : DataValueEditor, IDa protected IEnumerable GetBlockValueReferences(BlockValue blockValue) { - var result = new List(); - - // loop through all content and settings data - foreach (BlockItemData row in blockValue.ContentData.Concat(blockValue.SettingsData)) + 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)) { - foreach (KeyValuePair prop in row.PropertyValues) + if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor)) { - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - - IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) - { - continue; - } - - var val = prop.Value.Value?.ToString(); - - IEnumerable refs = reference.GetReferences(val); - - result.AddRange(refs); + continue; } - } - return result; + var districtValues = valuesByPropertyEditorAlias.Distinct().ToArray(); + + if (dataEditor.GetValueEditor() is IDataValueReference reference) + { + foreach (UmbracoEntityReference value in districtValues.SelectMany(reference.GetReferences)) + { + yield return value; + } + } + + IEnumerable references = _dataValueReferenceFactoryCollection.GetReferences(dataEditor, districtValues); + + foreach (UmbracoEntityReference value in references) + { + yield return value; + } + + } } /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index 1bfa3a7ed0..4cdbfeb419 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -91,9 +91,10 @@ public class NestedContentPropertyEditor : DataEditor internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference, IDataValueTags { private readonly IDataTypeService _dataTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories; private readonly ILogger _logger; private readonly NestedContentValues _nestedContentValues; - private readonly PropertyEditorCollection _propertyEditors; public NestedContentPropertyValueEditor( IDataTypeService dataTypeService, @@ -102,16 +103,19 @@ public class NestedContentPropertyEditor : DataEditor IShortStringHelper shortStringHelper, DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, ILogger logger, IJsonSerializer jsonSerializer, IIOHelper ioHelper, IPropertyValidationService propertyValidationService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { - _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; + _propertyEditors = propertyEditors; + _dataValueReferenceFactories = dataValueReferenceFactories; _logger = logger; _nestedContentValues = new NestedContentValues(contentTypeService); + Validators.Add(new NestedContentValidator(propertyValidationService, _nestedContentValues, contentTypeService)); } @@ -139,66 +143,47 @@ public class NestedContentPropertyEditor : DataEditor } } + /// public IEnumerable GetReferences(object? value) { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var result = new List(); - - foreach (NestedContentValues.NestedContentRowValue row in _nestedContentValues.GetPropertyValues(rawJson)) + // 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)) { - foreach (KeyValuePair prop in - row.PropertyValues) + if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor)) { - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + continue; + } - IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) - { - continue; - } - - var val = prop.Value.Value?.ToString(); - - IEnumerable refs = reference.GetReferences(val); - - result.AddRange(refs); + // Use distinct values to avoid duplicate parsing of the same value + foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct())) + { + yield return reference; } } - - return result; } /// public IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) { - IReadOnlyList rows = - _nestedContentValues.GetPropertyValues(value); - - var result = new List(); - - foreach (NestedContentValues.NestedContentRowValue row in rows.ToList()) + foreach (NestedContentValues.NestedContentPropertyValue propertyValue in GetAllPropertyValues(value)) { - foreach (KeyValuePair prop in row.PropertyValues - .ToList()) + if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) || + dataEditor.GetValueEditor() is not IDataValueTags dataValueTags) { - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + continue; + } - IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); - if (valueEditor is not IDataValueTags tagsProvider) - { - continue; - } - - object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration; - - result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId)); + object? configuration = _dataTypeService.GetDataType(propertyValue.PropertyType.DataTypeKey)?.Configuration; + foreach (ITag tag in dataValueTags.GetTags(propertyValue.Value, configuration, languageId)) + { + yield return tag; } } - - return result; } + private IEnumerable GetAllPropertyValues(object? value) + => _nestedContentValues.GetPropertyValues(value).SelectMany(x => x.PropertyValues.Values); + #region DB to String public override string ConvertDbToString(IPropertyType propertyType, object? propertyValue) @@ -424,7 +409,8 @@ public class NestedContentPropertyEditor : DataEditor // set values to null row.PropertyValues[elementTypeProp.Alias] = new NestedContentValues.NestedContentPropertyValue { - PropertyType = elementTypeProp, Value = null, + PropertyType = elementTypeProp, + Value = null, }; row.RawPropertyValues[elementTypeProp.Alias] = null; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 8d38b218b5..d06fa9ffe1 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -189,8 +189,9 @@ public class RichTextPropertyEditor : DataEditor IHtmlSanitizer htmlSanitizer, IHtmlMacroParameterParser macroParameterParser, IContentTypeService contentTypeService, - IPropertyValidationService propertyValidationService) - : base(attribute, propertyEditors, dataTypeService, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper) + IPropertyValidationService propertyValidationService, + DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection) + : base(attribute, propertyEditors, dataTypeService, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _imageSourceParser = imageSourceParser; diff --git a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs index d854de4b95..6897f04d72 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs @@ -36,7 +36,7 @@ public class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigab IDomainCache domainCache, IOptions globalSettings, IVariationContextAccessor variationContextAccessor) - : base(previewDefault) + : base(variationContextAccessor, previewDefault) { _snapshot = snapshot; _snapshotCache = snapshotCache; diff --git a/src/Umbraco.PublishedCache.NuCache/MediaCache.cs b/src/Umbraco.PublishedCache.NuCache/MediaCache.cs index 014140e884..516d2f555e 100644 --- a/src/Umbraco.PublishedCache.NuCache/MediaCache.cs +++ b/src/Umbraco.PublishedCache.NuCache/MediaCache.cs @@ -17,7 +17,7 @@ public class MediaCache : PublishedCacheBase, IPublishedMediaCache, INavigableDa #region Constructors public MediaCache(bool previewDefault, ContentStore.Snapshot snapshot, IVariationContextAccessor variationContextAccessor) - : base(previewDefault) + : base(variationContextAccessor, previewDefault) { _snapshot = snapshot; _variationContextAccessor = variationContextAccessor; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs index 67dd5162bf..f4949c2fb6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -16,6 +15,7 @@ public class DataValueEditorReuseTests { private Mock _dataValueEditorFactoryMock; private PropertyEditorCollection _propertyEditorCollection; + private DataValueReferenceFactoryCollection _dataValueReferenceFactories; [SetUp] public void SetUp() @@ -32,6 +32,7 @@ public class DataValueEditorReuseTests Mock.Of())); _propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)); + _dataValueReferenceFactories = new DataValueReferenceFactoryCollection(Enumerable.Empty); _dataValueEditorFactoryMock .Setup(m => @@ -40,6 +41,7 @@ public class DataValueEditorReuseTests new DataEditorAttribute("a", "b", "c"), new BlockListEditorDataConverter(), _propertyEditorCollection, + _dataValueReferenceFactories, Mock.Of(), Mock.Of(), Mock.Of(), @@ -95,7 +97,7 @@ public class DataValueEditorReuseTests { var blockListPropertyEditor = new BlockListPropertyEditor( _dataValueEditorFactoryMock.Object, - new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)), + _propertyEditorCollection, Mock.Of(), Mock.Of(), Mock.Of()); @@ -116,7 +118,7 @@ public class DataValueEditorReuseTests { var blockListPropertyEditor = new BlockListPropertyEditor( _dataValueEditorFactoryMock.Object, - new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)), + _propertyEditorCollection, Mock.Of(), Mock.Of(), Mock.Of()); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs index cff072873f..38fc5125dc 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs @@ -176,14 +176,11 @@ public class DataValueReferenceFactoryCollectionTests { var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty); var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)); - var properties = new PropertyCollection(); - var resultA = collection.GetAutomaticRelationTypesAliases(propertyEditors).ToArray(); - var resultB = collection.GetAutomaticRelationTypesAliases(properties, propertyEditors).ToArray(); + var result = collection.GetAllAutomaticRelationTypesAliases(propertyEditors).ToArray(); var expected = Constants.Conventions.RelationTypes.AutomaticRelationTypes; - CollectionAssert.AreEquivalent(expected, resultA, "Result A does not contain the expected relation type aliases."); - CollectionAssert.AreEquivalent(expected, resultB, "Result B does not contain the expected relation type aliases."); + CollectionAssert.AreEquivalent(expected, result, "Result does not contain the expected relation type aliases."); } [Test] @@ -194,15 +191,11 @@ public class DataValueReferenceFactoryCollectionTests var labelPropertyEditor = new LabelPropertyEditor(DataValueEditorFactory, IOHelper, EditorConfigurationParser); var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => labelPropertyEditor.Yield())); var serializer = new ConfigurationEditorJsonSerializer(); - var property = new Property(new PropertyType(ShortStringHelper, new DataType(labelPropertyEditor, serializer))); - var properties = new PropertyCollection { property, property }; // Duplicate on purpose to test distinct aliases - var resultA = collection.GetAutomaticRelationTypesAliases(propertyEditors).ToArray(); - var resultB = collection.GetAutomaticRelationTypesAliases(properties, propertyEditors).ToArray(); + var result = collection.GetAllAutomaticRelationTypesAliases(propertyEditors).ToArray(); var expected = Constants.Conventions.RelationTypes.AutomaticRelationTypes.Append("umbTest"); - CollectionAssert.AreEquivalent(expected, resultA, "Result A does not contain the expected relation type aliases."); - CollectionAssert.AreEquivalent(expected, resultB, "Result B does not contain the expected relation type aliases."); + CollectionAssert.AreEquivalent(expected, result, "Result does not contain the expected relation type aliases."); } private class TestDataValueReferenceFactory : IDataValueReferenceFactory