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