refactor(core): expose PerformMoveLocked on IContentMoveOperationService

Make existing private PerformMoveLocked method public and add to interface.
Update ContentService.MoveToRecycleBin and DeleteOfTypes to delegate to the service.
Remove duplicate helper methods from ContentService:
- PerformMoveLocked
- PerformMoveContentLocked
- GetPagedDescendantQuery
- GetPagedLocked

This reduces ContentService complexity while maintaining MoveToRecycleBin
orchestration in the facade.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-24 19:32:26 +00:00
parent e514b703aa
commit d09b726f9d
3 changed files with 34 additions and 101 deletions

View File

@@ -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
}
}
/// <inheritdoc />
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();
}
/// <summary>
/// Performs the actual move operation within an existing write lock.
/// </summary>
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;

View File

@@ -432,37 +432,6 @@ public class ContentService : RepositoryService, IContentService
public IEnumerable<IContent> GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, IQuery<IContent>? filter = null, Ordering? ordering = null)
=> CrudService.GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, filter, ordering);
private IQuery<IContent>? GetPagedDescendantQuery(string contentPath)
{
IQuery<IContent>? query = Query<IContent>();
if (!contentPath.IsNullOrWhiteSpace())
{
query?.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
}
return query;
}
private IEnumerable<IContent> GetPagedLocked(IQuery<IContent>? query, long pageIndex, int pageSize, out long totalChildren, IQuery<IContent>? 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);
}
/// <summary>
/// Gets the parent of the current content as an <see cref="IContent" /> item.
/// </summary>
@@ -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<int, string>();
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<IContent>? 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<IContent> 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<OperationResult> 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<IContent>(content, TreeChangeTypes.RefreshBranch));
}

View File

@@ -159,4 +159,21 @@ public interface IContentMoveOperationService : IService
OperationResult Sort(IEnumerable<int>? ids, int userId = Constants.Security.SuperUserId);
#endregion
#region Internal Move Operations
/// <summary>
/// Performs the locked move operation for a content item and its descendants.
/// Used internally by MoveToRecycleBin orchestration.
/// </summary>
/// <param name="content">The content to move.</param>
/// <param name="parentId">The target parent id.</param>
/// <param name="parent">The target parent content (can be null for root/recycle bin).</param>
/// <param name="userId">The user performing the operation.</param>
/// <param name="trash">Whether to mark as trashed (true), un-trashed (false), or unchanged (null).</param>
/// <returns>Collection of moved items with their original paths.</returns>
IReadOnlyCollection<(IContent Content, string OriginalPath)> PerformMoveLocked(
IContent content, int parentId, IContent? parent, int userId, bool? trash);
#endregion
}