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.");