diff --git a/src/Umbraco.Core/Services/ContentCrudService.cs b/src/Umbraco.Core/Services/ContentCrudService.cs
index 79e9f6ecd4..20a89bac50 100644
--- a/src/Umbraco.Core/Services/ContentCrudService.cs
+++ b/src/Umbraco.Core/Services/ContentCrudService.cs
@@ -634,7 +634,7 @@ public class ContentCrudService : ContentServiceBase, IContentCrudService
/// Uses paging to handle large trees without loading everything into memory.
/// Iteration bound prevents infinite loops if database is in inconsistent state.
///
- private void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs)
+ public void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs)
{
void DoDelete(IContent c)
{
diff --git a/src/Umbraco.Core/Services/ContentMoveOperationService.cs b/src/Umbraco.Core/Services/ContentMoveOperationService.cs
index 894a3ed38a..04c97ee7db 100644
--- a/src/Umbraco.Core/Services/ContentMoveOperationService.cs
+++ b/src/Umbraco.Core/Services/ContentMoveOperationService.cs
@@ -251,7 +251,7 @@ public class ContentMoveOperationService : ContentServiceBase, IContentMoveOpera
continue;
}
- DeleteLocked(scope, content, eventMessages);
+ _crudService.DeleteLocked(scope, content, eventMessages);
deleted.Add(content);
}
}
@@ -298,64 +298,6 @@ public class ContentMoveOperationService : ContentServiceBase, IContentMoveOpera
}
}
- ///
- /// Deletes content and all descendants within an existing scope.
- ///
- private void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs)
- {
- void DoDelete(IContent c)
- {
- DocumentRepository.Delete(c);
- scope.Notifications.Publish(new ContentDeletedNotification(c, evtMsgs));
- }
-
- // v1.1: Using class-level constants
- var iteration = 0;
- var total = long.MaxValue;
-
- while (total > 0 && iteration < MaxDeleteIterations)
- {
- IEnumerable descendants = GetPagedDescendantsLocked(
- content.Id,
- 0,
- DefaultPageSize,
- out total,
- ordering: Ordering.By("Path", Direction.Descending));
-
- var batch = descendants.ToList();
-
- // v1.1: Break immediately when batch is empty (fix from critical review 2.5)
- if (batch.Count == 0)
- {
- if (total > 0)
- {
- _logger.LogWarning(
- "GetPagedDescendants reported {Total} total descendants but returned empty batch for content {ContentId}. Breaking loop.",
- total,
- content.Id);
- }
- break; // Break immediately, don't continue iterating
- }
-
- foreach (IContent c in batch)
- {
- DoDelete(c);
- }
-
- iteration++;
- }
-
- if (iteration >= MaxDeleteIterations)
- {
- _logger.LogError(
- "DeleteLocked exceeded maximum iteration limit ({MaxIterations}) for content {ContentId}. Tree may be incompletely deleted.",
- MaxDeleteIterations,
- content.Id);
- }
-
- DoDelete(content);
- }
-
#endregion
#region Copy Operations
diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index cb59a8463e..9ccf5a31c5 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -555,31 +555,6 @@ public class ContentService : RepositoryService, IContentService
public OperationResult Delete(IContent content, int userId = Constants.Security.SuperUserId)
=> CrudService.Delete(content, userId);
- private void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs)
- {
- void DoDelete(IContent c)
- {
- _documentRepository.Delete(c);
- scope.Notifications.Publish(new ContentDeletedNotification(c, evtMsgs));
-
- // media files deleted by QueuingEventDispatcher
- }
-
- const int pageSize = 500;
- var total = long.MaxValue;
- while (total > 0)
- {
- // get descendants - ordered from deepest to shallowest
- IEnumerable descendants = GetPagedDescendants(content.Id, 0, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending));
- foreach (IContent c in descendants)
- {
- DoDelete(c);
- }
- }
-
- DoDelete(content);
- }
-
// TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way
// Delete does - for a good reason: the file may be referenced by other, non-deleted, versions. BUT,
// if that's not the case, then the file will never be deleted, because when we delete the content,
@@ -881,7 +856,7 @@ public class ContentService : RepositoryService, IContentService
// delete content
// triggers the deleted event (and handles the files)
- DeleteLocked(scope, content, eventMessages);
+ CrudService.DeleteLocked(scope, content, eventMessages);
changes.Add(new TreeChange(content, TreeChangeTypes.Remove));
}
diff --git a/src/Umbraco.Core/Services/IContentCrudService.cs b/src/Umbraco.Core/Services/IContentCrudService.cs
index 86d993a2da..b7f8d06330 100644
--- a/src/Umbraco.Core/Services/IContentCrudService.cs
+++ b/src/Umbraco.Core/Services/IContentCrudService.cs
@@ -1,6 +1,8 @@
// src/Umbraco.Core/Services/IContentCrudService.cs
+using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
+using Umbraco.Cms.Core.Scoping;
namespace Umbraco.Cms.Core.Services;
@@ -247,5 +249,14 @@ public interface IContentCrudService : IService
/// The operation result.
OperationResult Delete(IContent content, int userId = Constants.Security.SuperUserId);
+ ///
+ /// Performs the locked delete operation including descendants.
+ /// Used internally by DeleteOfTypes orchestration and EmptyRecycleBin.
+ ///
+ /// The active scope with write lock.
+ /// The document to delete.
+ /// Event messages collection.
+ void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs);
+
#endregion
}