Files
Umbraco-CMS/docs/plans/2025-12-19-contentservice-refactor-design-critical-review-1.md
yv01p 9962df50ee feat(core): register IContentCrudService in DI container
Adds IContentCrudService registration to UmbracoBuilder alongside
IContentService. Both services are now resolvable from DI.

Includes integration test verifying successful resolution.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-21 03:31:23 +00:00

9.3 KiB

Critical Architectural Review: ContentService Refactoring Design

Reviewed Document: docs/plans/2025-12-19-contentservice-refactor-design.md Review Date: 2025-12-19 Reviewer Role: Senior Principal Software Architect


1. Overall Assessment

Strengths:

  • Correctly identifies a real maintainability problem (~3800 lines monolith)
  • Preserves backward compatibility via facade pattern (essential for Umbraco ecosystem)
  • Clear separation by functional domain (CRUD, Publishing, Move)
  • Aligns with Umbraco's existing layered architecture (interfaces in Core, implementations in Infrastructure)

Major Concerns:

  • Naming collision with existing ContentPublishingService already in codebase
  • Cross-service transaction coordination inadequately addressed
  • Arbitrary public/internal distinction doesn't match actual usage patterns
  • Missing mapping for several existing methods that don't fit cleanly into proposed services

2. Critical Issues

2.1 Naming Collision with Existing ContentPublishingService

Description: A ContentPublishingService already exists at src/Umbraco.Core/Services/ContentPublishingService.cs:16 with interface IContentPublishingService. The proposed IContentPublishingService would directly conflict.

Impact: Build failure or confusing namespace disambiguation; existing consumers of IContentPublishingService would break or become ambiguous.

Suggestion: Rename the proposed interface to IContentPublishOperationService or IContentPublicationService, OR refactor the existing ContentPublishingService to use the new infrastructure.


2.2 Cross-Service Dependencies Create Circular Risk

Description: The proposed services have intrinsic coupling:

  • MoveToRecycleBin (Move) fires ContentUnpublishedNotification for published content (requires Publishing awareness)
  • Publish (Publishing) calls Save internally (requires CRUD)
  • Copy (Move) calls Save and checks GetPermissions (requires CRUD and Permissions)

Impact: Either circular dependencies between services OR the facade must orchestrate complex multi-service operations, defeating decomposition benefits.

Suggestion: Extract a shared ContentOperationOrchestrator that coordinates cross-cutting operations, or make Publishing/Move depend on CRUD (unidirectional), with explicit dependency documentation.


2.3 Transaction Boundary Ownership Unclear

Description: The design states "Ensure scopes work across service calls" but doesn't specify:

  • Who creates the scope when ContentMoveService.MoveToRecycleBin needs to unpublish content?
  • Can services assume they're called within an existing scope, or must each method create its own?
  • How do nested scope behaviors work when Service A calls Service B?

Impact: Inconsistent transaction handling could lead to partial commits, audit log inconsistencies, or notification ordering issues.

Suggestion: Define explicit scope ownership rules:

  • Option A: All public service methods create their own scope (simple, but nested operations may have issues)
  • Option B: Services accept optional ICoreScope parameter for caller-managed transactions
  • Option C: Use ambient scope pattern consistently (document the pattern)

2.4 Arbitrary Public vs Internal Classification

Description: The design classifies:

  • Public: CRUD, Publishing, Move
  • Internal: Versioning, Query, Permission, Blueprint

But examining actual usage:

  • GetVersions, Rollback, DeleteVersions are public on IContentService
  • GetPermissions, SetPermissions are public on IContentService
  • GetPagedChildren, GetAncestors, Count are frequently used externally
  • Blueprints have public API methods

Impact: Internal helpers cannot be injected by consumers who need specific functionality. Forces continued use of IContentService facade for everything.

Suggestion: Either:

  • Make Query and Versioning public (they represent distinct concerns API consumers need)
  • OR keep all as internal and document that IContentService remains the public API (current approach but explicitly stated)

2.5 Missing Method Mappings

Description: Several existing methods don't cleanly map to proposed services:

Method Proposed Location Problem
SendToPublication ? Not listed, involves Save + events
CheckDataIntegrity ? Not listed, infrastructure concern
DeleteOfTypes CRUD? Move? Moves children to bin, then deletes
CommitDocumentChanges Publishing? Internal but critical for scheduling
GetPublishedChildren Query? Publishing? Uses Published status (domain crossover)

Impact: Implementation will encounter methods that don't fit, leading to ad-hoc placement or leaky abstractions.

Suggestion: Create explicit method-to-service mapping table covering ALL 80+ methods in current IContentService. Identify "orchestration" methods that may stay in facade.


2.6 Notification Consistency Risk

Description: ContentService fires ~20 different notification types (ContentSaving, ContentPublishing, ContentMoving, etc.) with specific state propagation via WithStateFrom(). Splitting services means:

  • Each service must correctly fire its subset of notifications
  • State must be preserved across service boundaries
  • Notification ordering must remain consistent

Impact: Breaking notification contracts would affect cache invalidation, webhooks, search indexing, and third-party packages.

Suggestion: Add explicit notification responsibility matrix to design. Consider a ContentNotificationService that centralizes notification logic, called by all sub-services.


3. Alternative Architectural Challenge

Alternative: Vertical Slicing by Operation Complexity

Instead of horizontal domain slicing (CRUD/Publishing/Move), slice vertically by operation complexity:

Proposed Structure:

  1. ContentReadService - All read operations (Get, Count, Query, Versions read-only)
  2. ContentWriteService - All simple mutations (Create, Save, Delete)
  3. ContentWorkflowService - All complex stateful operations (Publish, Unpublish, Schedule, Move, MoveToRecycleBin)

Pro: Matches actual dependency patterns - reads are independent, writes have minimal coupling, workflows can depend on both. Simpler transaction model.

Con: Workflows service could still grow large; doesn't solve the "where does Copy go" problem.


4. Minor Issues & Improvements

4.1 Line Count Estimates May Be Optimistic

The ContentPublishingService is estimated at ~800 lines, but CommitDocumentChangesInternal alone is 330+ lines with complex culture/scheduling logic. Consider splitting publishing into Immediate vs Scheduled sub-components.

4.2 Missing Async Consideration

Existing EmptyRecycleBinAsync suggests async patterns are emerging. New services should define async-first interfaces where database operations are involved.

4.3 No Explicit Interface for ContentServiceBase

The design shows ContentServiceBase as abstract class but doesn't specify if it implements any interface. Consider IContentServiceBase for testing.

4.4 Helper Naming Convention

"Helper" suffix is discouraged in modern .NET. Consider ContentVersionManager, ContentQueryExecutor, etc.


5. Questions for Clarification

  1. Existing IContentPublishingService: Should the current ContentPublishingService be refactored to use the new infrastructure, replaced, or renamed?

  2. Branch Publishing: The complex PublishBranch operation (200+ lines) spans publishing AND tree traversal. Which service owns it?

  3. Locking Strategy: Current code uses explicit scope.WriteLock(Constants.Locks.ContentTree). Will each sub-service acquire locks independently, or is there a coordination pattern?

  4. Culture-Variant Complexity: The CommitDocumentChangesInternal method handles 15+ different publish result types with culture awareness. Is this complexity remaining in one place or distributed?

  5. RepositoryService Base Class: Current ContentService : RepositoryService. Do new services also inherit this, or get dependencies differently?


6. Final Recommendation

Major Revisions Needed

The design addresses a real problem and the overall direction is sound, but implementation will hit significant obstacles without addressing the following:

Priority Action Item Status
P0 Resolve naming collision with existing ContentPublishingService Required
P0 Document transaction/scope ownership explicitly Required
P0 Create complete method mapping covering all 80+ IContentService methods Required
P1 Define notification responsibility matrix per service Required
P1 Reconsider public/internal classification based on actual API usage patterns Recommended
P1 Address cross-service dependency direction (who can call whom) Recommended

Once these are addressed, the refactoring approach is viable and would meaningfully improve maintainability.


Appendix: Key Files Reviewed

  • src/Umbraco.Core/Services/ContentService.cs (3824 lines)
  • src/Umbraco.Core/Services/IContentService.cs (522 lines, ~80 methods)
  • src/Umbraco.Core/Services/ContentPublishingService.cs (existing, conflicts with proposal)
  • src/Umbraco.Core/CLAUDE.md (architecture patterns reference)