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>
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
ContentPublishingServicealready 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) firesContentUnpublishedNotificationfor published content (requires Publishing awareness)Publish(Publishing) callsSaveinternally (requires CRUD)Copy(Move) callsSaveand checksGetPermissions(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.MoveToRecycleBinneeds 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
ICoreScopeparameter 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,DeleteVersionsare public onIContentServiceGetPermissions,SetPermissionsare public onIContentServiceGetPagedChildren,GetAncestors,Countare 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
IContentServiceremains 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:
ContentReadService- All read operations (Get, Count, Query, Versions read-only)ContentWriteService- All simple mutations (Create, Save, Delete)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
-
Existing IContentPublishingService: Should the current
ContentPublishingServicebe refactored to use the new infrastructure, replaced, or renamed? -
Branch Publishing: The complex
PublishBranchoperation (200+ lines) spans publishing AND tree traversal. Which service owns it? -
Locking Strategy: Current code uses explicit
scope.WriteLock(Constants.Locks.ContentTree). Will each sub-service acquire locks independently, or is there a coordination pattern? -
Culture-Variant Complexity: The
CommitDocumentChangesInternalmethod handles 15+ different publish result types with culture awareness. Is this complexity remaining in one place or distributed? -
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)