feat(core): add ContentServiceBase abstract class for Phase 1

Establishes shared infrastructure for content services:
- Common dependencies (DocumentRepository, AuditService, UserIdKeyResolver)
- Audit helper methods (sync and async)
- Inherits from RepositoryService for scope/query support
- Adds ContentServiceConstants for shared constants (batch page size)

Updated tracking test to look for correct assembly location.
Tracking test now fails (expected) - signals class exists for future work.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-21 00:19:09 +00:00
parent a079c44afb
commit c9ff758aca
3 changed files with 82 additions and 1 deletions

View File

@@ -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;
/// <summary>
/// Abstract base class for content-related services providing shared infrastructure.
/// </summary>
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));
}
/// <summary>
/// Records an audit entry for a content operation (synchronous).
/// </summary>
/// <remarks>
/// Uses ConfigureAwait(false) to avoid capturing synchronization context and prevent deadlocks.
/// TODO: Replace with sync overloads when IAuditService.Add and IUserIdKeyResolver.Get are available.
/// </remarks>
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();
}
/// <summary>
/// Records an audit entry for a content operation asynchronously.
/// </summary>
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);
}
}

View File

@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Core.Services;
/// <summary>
/// Constants used by content-related services.
/// </summary>
public static class ContentServiceConstants
{
/// <summary>
/// Default page size for batch operations (e.g., cascade delete).
/// </summary>
public const int DefaultBatchPageSize = 500;
}