# 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)