diff --git a/src/Umbraco.Core/Services/ContentMoveOperationService.cs b/src/Umbraco.Core/Services/ContentMoveOperationService.cs
index 2ad96f2eab..894a3ed38a 100644
--- a/src/Umbraco.Core/Services/ContentMoveOperationService.cs
+++ b/src/Umbraco.Core/Services/ContentMoveOperationService.cs
@@ -111,7 +111,7 @@ public class ContentMoveOperationService : ContentServiceBase, IContentMoveOpera
content.PublishedState = PublishedState.Unpublishing;
}
- PerformMoveLocked(content, parentId, parent, userId, moves, trashed);
+ PerformMoveLockedInternal(content, parentId, parent, userId, moves, trashed);
scope.Notifications.Publish(
new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
@@ -134,10 +134,19 @@ public class ContentMoveOperationService : ContentServiceBase, IContentMoveOpera
}
}
+ ///
+ public IReadOnlyCollection<(IContent Content, string OriginalPath)> PerformMoveLocked(
+ IContent content, int parentId, IContent? parent, int userId, bool? trash)
+ {
+ var moves = new List<(IContent, string)>();
+ PerformMoveLockedInternal(content, parentId, parent, userId, moves, trash);
+ return moves.AsReadOnly();
+ }
+
///
/// Performs the actual move operation within an existing write lock.
///
- private void PerformMoveLocked(IContent content, int parentId, IContent? parent, int userId, ICollection<(IContent, string)> moves, bool? trash)
+ private void PerformMoveLockedInternal(IContent content, int parentId, IContent? parent, int userId, ICollection<(IContent, string)> moves, bool? trash)
{
content.WriterId = userId;
content.ParentId = parentId;
diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index 7379e3d923..cb59a8463e 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -432,37 +432,6 @@ public class ContentService : RepositoryService, IContentService
public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, IQuery? filter = null, Ordering? ordering = null)
=> CrudService.GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, filter, ordering);
- private IQuery? GetPagedDescendantQuery(string contentPath)
- {
- IQuery? query = Query();
- if (!contentPath.IsNullOrWhiteSpace())
- {
- query?.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
- }
-
- return query;
- }
-
- private IEnumerable GetPagedLocked(IQuery? query, long pageIndex, int pageSize, out long totalChildren, IQuery? filter, Ordering? ordering)
- {
- if (pageIndex < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(pageIndex));
- }
-
- if (pageSize <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(pageSize));
- }
-
- if (ordering == null)
- {
- throw new ArgumentNullException(nameof(ordering));
- }
-
- return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
- }
-
///
/// Gets the parent of the current content as an item.
///
@@ -645,7 +614,6 @@ public class ContentService : RepositoryService, IContentService
public OperationResult MoveToRecycleBin(IContent content, int userId = Constants.Security.SuperUserId)
{
EventMessages eventMessages = EventMessagesFactory.Get();
- var moves = new List<(IContent, string)>();
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
@@ -668,7 +636,7 @@ public class ContentService : RepositoryService, IContentService
// it's NOT unpublishing, only the content is now masked - allowing us to restore it if wanted
// if (content.HasPublishedVersion)
// { }
- PerformMoveLocked(content, Constants.System.RecycleBinContent, null, userId, moves, true);
+ var moves = MoveOperationService.PerformMoveLocked(content, Constants.System.RecycleBinContent, null, userId, true);
scope.Notifications.Publish(
new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
@@ -709,71 +677,6 @@ public class ContentService : RepositoryService, IContentService
return MoveOperationService.Move(content, parentId, userId);
}
- // MUST be called from within WriteLock
- // trash indicates whether we are trashing, un-trashing, or not changing anything
- private void PerformMoveLocked(IContent content, int parentId, IContent? parent, int userId, ICollection<(IContent, string)> moves, bool? trash)
- {
- content.WriterId = userId;
- content.ParentId = parentId;
-
- // get the level delta (old pos to new pos)
- // note that recycle bin (id:-20) level is 0!
- var levelDelta = 1 - content.Level + (parent?.Level ?? 0);
-
- var paths = new Dictionary();
-
- moves.Add((content, content.Path)); // capture original path
-
- // need to store the original path to lookup descendants based on it below
- var originalPath = content.Path;
-
- // these will be updated by the repo because we changed parentId
- // content.Path = (parent == null ? "-1" : parent.Path) + "," + content.Id;
- // content.SortOrder = ((ContentRepository) repository).NextChildSortOrder(parentId);
- // content.Level += levelDelta;
- PerformMoveContentLocked(content, userId, trash);
-
- // if uow is not immediate, content.Path will be updated only when the UOW commits,
- // and because we want it now, we have to calculate it by ourselves
- // paths[content.Id] = content.Path;
- paths[content.Id] =
- (parent == null
- ? parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString
- : parent.Path) + "," + content.Id;
-
- const int pageSize = 500;
- IQuery? query = GetPagedDescendantQuery(originalPath);
- long total;
- do
- {
- // We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced
- IEnumerable descendants =
- GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path"));
-
- foreach (IContent descendant in descendants)
- {
- moves.Add((descendant, descendant.Path)); // capture original path
-
- // update path and level since we do not update parentId
- descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id;
- descendant.Level += levelDelta;
- PerformMoveContentLocked(descendant, userId, trash);
- }
- }
- while (total > pageSize);
- }
-
- private void PerformMoveContentLocked(IContent content, int userId, bool? trash)
- {
- if (trash.HasValue)
- {
- ((ContentBase)content).Trashed = trash.Value;
- }
-
- content.WriterId = userId;
- _documentRepository.Save(content);
- }
-
public async Task EmptyRecycleBinAsync(Guid userId)
=> await MoveOperationService.EmptyRecycleBinAsync(userId);
@@ -968,7 +871,11 @@ public class ContentService : RepositoryService, IContentService
foreach (IContent child in children)
{
// see MoveToRecycleBin
- PerformMoveLocked(child, Constants.System.RecycleBinContent, null, userId, moves, true);
+ var childMoves = MoveOperationService.PerformMoveLocked(child, Constants.System.RecycleBinContent, null, userId, true);
+ foreach (var move in childMoves)
+ {
+ moves.Add(move);
+ }
changes.Add(new TreeChange(content, TreeChangeTypes.RefreshBranch));
}
diff --git a/src/Umbraco.Core/Services/IContentMoveOperationService.cs b/src/Umbraco.Core/Services/IContentMoveOperationService.cs
index 5f82f7a8f8..da8dbf52f9 100644
--- a/src/Umbraco.Core/Services/IContentMoveOperationService.cs
+++ b/src/Umbraco.Core/Services/IContentMoveOperationService.cs
@@ -159,4 +159,21 @@ public interface IContentMoveOperationService : IService
OperationResult Sort(IEnumerable? ids, int userId = Constants.Security.SuperUserId);
#endregion
+
+ #region Internal Move Operations
+
+ ///
+ /// Performs the locked move operation for a content item and its descendants.
+ /// Used internally by MoveToRecycleBin orchestration.
+ ///
+ /// The content to move.
+ /// The target parent id.
+ /// The target parent content (can be null for root/recycle bin).
+ /// The user performing the operation.
+ /// Whether to mark as trashed (true), un-trashed (false), or unchanged (null).
+ /// Collection of moved items with their original paths.
+ IReadOnlyCollection<(IContent Content, string OriginalPath)> PerformMoveLocked(
+ IContent content, int parentId, IContent? parent, int userId, bool? trash);
+
+ #endregion
}