Files
Umbraco-CMS/docs/plans/2025-12-19-contentservice-refactor-design-critical-review-1.md

193 lines
9.3 KiB
Markdown
Raw Normal View History

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