diff --git a/src/Umbraco.Core/Notifications/ContentScaffoldedNotification.cs b/src/Umbraco.Core/Notifications/ContentScaffoldedNotification.cs
new file mode 100644
index 0000000000..47eda5468d
--- /dev/null
+++ b/src/Umbraco.Core/Notifications/ContentScaffoldedNotification.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Notifications;
+
+///
+/// Notification that is send out when a Content item has been scaffolded from an original item and basic cleaning has been performed
+///
+public sealed class ContentScaffoldedNotification : ScaffoldedNotification
+{
+ public ContentScaffoldedNotification(IContent original, IContent scaffold, int parentId, EventMessages messages)
+ : base(original, scaffold, parentId, messages)
+ {
+ }
+}
diff --git a/src/Umbraco.Core/Notifications/ScaffoldedNotification.cs b/src/Umbraco.Core/Notifications/ScaffoldedNotification.cs
new file mode 100644
index 0000000000..f64bfd3933
--- /dev/null
+++ b/src/Umbraco.Core/Notifications/ScaffoldedNotification.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Umbraco.Cms.Core.Events;
+
+namespace Umbraco.Cms.Core.Notifications;
+
+public abstract class ScaffoldedNotification : CancelableObjectNotification
+ where T : class
+{
+ protected ScaffoldedNotification(T original, T scaffold, int parentId, EventMessages messages)
+ : base(original, messages)
+ {
+ Scaffold = scaffold;
+ ParentId = parentId;
+ }
+
+ public T Original => Target;
+
+ public T Scaffold { get; }
+
+ public int ParentId { get; }
+}
diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs
index c173e766e0..e15f581809 100644
--- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs
+++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs
@@ -32,15 +32,23 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase();
- 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;
}
- // Only use edited value for now
- references.UnionWith(GetReferences(dataEditor, property.Values.Select(x => x.EditedValue)));
+ // 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))
+ {
+ values.Add(propertyValue.EditedValue);
+ values.Add(propertyValue.PublishedValue);
+ }
+
+ references.UnionWith(GetReferences(dataEditor, values));
}
return references;
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/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
index d6531ca6e4..b7f83d7162 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
@@ -347,10 +347,13 @@ public static partial class UmbracoBuilderExtensions
builder
.AddNotificationHandler()
.AddNotificationHandler()
+ .AddNotificationHandler()
.AddNotificationHandler()
.AddNotificationHandler()
+ .AddNotificationHandler()
.AddNotificationHandler()
.AddNotificationHandler()
+ .AddNotificationHandler()
.AddNotificationHandler()
.AddNotificationHandler()
.AddNotificationHandler()
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
index 6569ad4d89..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.GetAllAutomaticRelationTypesAliases(PropertyEditors);
- RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray());
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 9739553b99..c281b0758e 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs
@@ -48,14 +48,16 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV
///
public IEnumerable GetReferences(object? value)
{
- foreach (BlockItemData.BlockPropertyValue propertyValue in GetAllPropertyValues(value))
+ // Group by property editor alias to avoid duplicate lookups and optimize value parsing
+ foreach (var valuesByPropertyEditorAlias in GetAllPropertyValues(value).GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value))
{
- if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor))
+ if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
{
continue;
}
- foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, propertyValue.Value))
+ // Use distinct values to avoid duplicate parsing of the same value
+ foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct()))
{
yield return reference;
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs
index 2b4ac75042..ce867d29e4 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs
@@ -10,9 +10,16 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
+///
+/// Handles nested Udi keys when
+/// - saving: Empty keys get generated
+/// - copy: keys get replaced by new ones while keeping references intact
+/// - scaffolding: keys get replaced by new ones while keeping references intact
+///
public abstract class ComplexPropertyEditorContentNotificationHandler :
INotificationHandler,
- INotificationHandler
+ INotificationHandler,
+ INotificationHandler
{
protected abstract string EditorAlias { get; }
@@ -31,6 +38,12 @@ public abstract class ComplexPropertyEditorContentNotificationHandler :
}
}
+ public void Handle(ContentScaffoldedNotification notification)
+ {
+ IEnumerable props = notification.Scaffold.GetPropertiesByEditor(EditorAlias);
+ UpdatePropertyValues(props, false);
+ }
+
protected abstract string FormatPropertyValue(string rawJson, bool onlyMissingKeys);
private void UpdatePropertyValues(IEnumerable props, bool onlyMissingKeys)
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
index 87d80e3972..b6eae8cb9f 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
@@ -146,14 +146,16 @@ public class NestedContentPropertyEditor : DataEditor
///
public IEnumerable GetReferences(object? value)
{
- foreach (NestedContentValues.NestedContentPropertyValue propertyValue in GetAllPropertyValues(value))
+ // Group by property editor alias to avoid duplicate lookups and optimize value parsing
+ foreach (var valuesByPropertyEditorAlias in GetAllPropertyValues(value).GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value))
{
- if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor))
+ if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
{
continue;
}
- foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, propertyValue.Value))
+ // Use distinct values to avoid duplicate parsing of the same value
+ foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct()))
{
yield return reference;
}
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/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
index 6ba81172c8..a1ed75332b 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
@@ -19,6 +19,7 @@ using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Validation;
+using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Routing;
@@ -683,17 +684,33 @@ public class ContentController : ContentControllerBase
[OutgoingEditorModelEvent]
public ActionResult GetEmptyBlueprint(int blueprintId, int parentId)
{
- IContent? blueprint = _contentService.GetBlueprintById(blueprintId);
- if (blueprint == null)
+ IContent? scaffold;
+ using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
- return NotFound();
+ IContent? blueprint = _contentService.GetBlueprintById(blueprintId);
+ if (blueprint is null)
+ {
+ return NotFound();
+ }
+ scaffold = (IContent)blueprint.DeepClone();
+
+ scaffold.Id = 0;
+ scaffold.Name = string.Empty;
+ scaffold.ParentId = parentId;
+
+ var scaffoldedNotification = new ContentScaffoldedNotification(blueprint, scaffold, parentId, new EventMessages());
+ if (scope.Notifications.PublishCancelable(scaffoldedNotification))
+ {
+ scope.Complete();
+ return Problem("Scaffolding was cancelled");
+ }
+
+ scope.Complete();
}
- blueprint.Id = 0;
- blueprint.Name = string.Empty;
- blueprint.ParentId = parentId;
- ContentItemDisplay? mapped = _umbracoMapper.Map(blueprint);
+
+ ContentItemDisplay? mapped = _umbracoMapper.Map(scaffold);
if (mapped is not null)
{