diff --git a/src/Umbraco.Core/Services/ContentServiceBase.cs b/src/Umbraco.Core/Services/ContentServiceBase.cs
new file mode 100644
index 0000000000..4bcb9db056
--- /dev/null
+++ b/src/Umbraco.Core/Services/ContentServiceBase.cs
@@ -0,0 +1,69 @@
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Persistence.Querying;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.Scoping;
+
+namespace Umbraco.Cms.Core.Services;
+
+///
+/// Abstract base class for content-related services providing shared infrastructure.
+///
+public abstract class ContentServiceBase : RepositoryService
+{
+ protected readonly IDocumentRepository DocumentRepository;
+ protected readonly IAuditService AuditService;
+ protected readonly IUserIdKeyResolver UserIdKeyResolver;
+
+ protected ContentServiceBase(
+ ICoreScopeProvider provider,
+ ILoggerFactory loggerFactory,
+ IEventMessagesFactory eventMessagesFactory,
+ IDocumentRepository documentRepository,
+ IAuditService auditService,
+ IUserIdKeyResolver userIdKeyResolver)
+ : base(provider, loggerFactory, eventMessagesFactory)
+ {
+ DocumentRepository = documentRepository ?? throw new ArgumentNullException(nameof(documentRepository));
+ AuditService = auditService ?? throw new ArgumentNullException(nameof(auditService));
+ UserIdKeyResolver = userIdKeyResolver ?? throw new ArgumentNullException(nameof(userIdKeyResolver));
+ }
+
+ ///
+ /// Records an audit entry for a content operation (synchronous).
+ ///
+ ///
+ /// Uses ConfigureAwait(false) to avoid capturing synchronization context and prevent deadlocks.
+ /// TODO: Replace with sync overloads when IAuditService.Add and IUserIdKeyResolver.Get are available.
+ ///
+ protected void Audit(AuditType type, int userId, int objectId, string? message = null, string? parameters = null)
+ {
+ // Use ConfigureAwait(false) to avoid context capture and potential deadlocks
+ Guid userKey = UserIdKeyResolver.GetAsync(userId).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ AuditService.AddAsync(
+ type,
+ userKey,
+ objectId,
+ UmbracoObjectTypes.Document.GetName(),
+ message,
+ parameters).ConfigureAwait(false).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Records an audit entry for a content operation asynchronously.
+ ///
+ protected async Task AuditAsync(AuditType type, int userId, int objectId, string? message = null, string? parameters = null)
+ {
+ Guid userKey = await UserIdKeyResolver.GetAsync(userId).ConfigureAwait(false);
+
+ await AuditService.AddAsync(
+ type,
+ userKey,
+ objectId,
+ UmbracoObjectTypes.Document.GetName(),
+ message,
+ parameters).ConfigureAwait(false);
+ }
+}
diff --git a/src/Umbraco.Core/Services/ContentServiceConstants.cs b/src/Umbraco.Core/Services/ContentServiceConstants.cs
new file mode 100644
index 0000000000..f4e373b3b1
--- /dev/null
+++ b/src/Umbraco.Core/Services/ContentServiceConstants.cs
@@ -0,0 +1,12 @@
+namespace Umbraco.Cms.Core.Services;
+
+///
+/// Constants used by content-related services.
+///
+public static class ContentServiceConstants
+{
+ ///
+ /// Default page size for batch operations (e.g., cascade delete).
+ ///
+ public const int DefaultBatchPageSize = 500;
+}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/ContentServiceBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/ContentServiceBaseTests.cs
index 4921850d92..f89fa8064b 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/ContentServiceBaseTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/ContentServiceBaseTests.cs
@@ -223,7 +223,7 @@ public class ContentServiceBaseTests
// 2. Delete this tracking test
// 3. Verify all tests pass
- var type = Type.GetType("Umbraco.Cms.Infrastructure.Services.ContentServiceBase, Umbraco.Infrastructure");
+ var type = Type.GetType("Umbraco.Cms.Core.Services.ContentServiceBase, Umbraco.Core");
Assert.That(type, Is.Null,
"ContentServiceBase now exists! Uncomment all tests in this file and delete this tracking test.");