193 lines
9.3 KiB
Markdown
193 lines
9.3 KiB
Markdown
|
|
# 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)
|