docs: add Phase 8 implementation plan and review documents
- Update Phase 8 implementation plan (v6.0) - Add critical review documents (v5, v6) - Add Task 1 review document 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
# Critical Implementation Review - Phase 8 Implementation Plan v5.0
|
||||
|
||||
**Review Date:** 2025-12-24
|
||||
**Reviewer:** Claude (Senior Staff Engineer)
|
||||
**Plan Version:** 5.0
|
||||
**Review Number:** 5
|
||||
|
||||
---
|
||||
|
||||
## 1. Overall Assessment
|
||||
|
||||
**Strengths:**
|
||||
- Comprehensive version history with clear tracking of changes across 5 iterations
|
||||
- Well-documented execution order rationale with the Task 5 → 4 → 1 reordering
|
||||
- The v5.0 additions addressing DeleteOfTypes (Task 3 Step 4a) and internal caller updates (Task 3 Step 2a) are critical fixes
|
||||
- Detailed field analysis with clear keep/remove decisions
|
||||
- Good risk mitigation and rollback plan documentation
|
||||
- The new `IReadOnlyCollection` return type for PerformMoveLocked (v4.0) is a cleaner API design
|
||||
|
||||
**Major Concerns:**
|
||||
1. **GetAllPublished is used by integration tests** - The plan's Task 6 Step 1 checks for usage but the review missed that `tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexHelperTests.cs` uses `ContentService.GetAllPublished()` directly (line 116)
|
||||
2. **DeleteLocked implementations are not functionally equivalent** - ContentService version lacks iteration bounds and logging present in ContentCrudService version
|
||||
3. **Missing ICoreScope parameter import in interface** - The IContentCrudService.DeleteLocked signature uses ICoreScope but may need using statement verification
|
||||
|
||||
---
|
||||
|
||||
## 2. Critical Issues
|
||||
|
||||
### 2.1 CRITICAL: GetAllPublished Used by Integration Tests
|
||||
|
||||
**Description:** The plan (Task 6 Step 1) instructs to check for external usage of `GetAllPublished`, but the verification commands miss a real usage:
|
||||
|
||||
```
|
||||
tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexHelperTests.cs:116
|
||||
```
|
||||
|
||||
The test method `GetExpectedNumberOfContentItems()` directly calls `ContentService.GetAllPublished()`:
|
||||
```csharp
|
||||
var result = ContentService.GetAllPublished().Count();
|
||||
```
|
||||
|
||||
**Why it matters:** Removing `GetAllPublished` will break this test. Since tests use `InternalsVisibleTo`, internal methods are accessible.
|
||||
|
||||
**Actionable Fix:** Add to Task 6 Step 1:
|
||||
```markdown
|
||||
### Step 1b: Update or refactor test usage
|
||||
|
||||
If tests use `GetAllPublished`, either:
|
||||
1. **Keep the method** and document it as test-only internal infrastructure
|
||||
2. **Refactor the test** to use a different approach (e.g., query for published content via IContentQueryOperationService)
|
||||
3. **Create a test helper** that replicates this functionality for test purposes only
|
||||
|
||||
For `DeliveryApiContentIndexHelperTests.cs`, consider replacing:
|
||||
```csharp
|
||||
// FROM:
|
||||
var result = ContentService.GetAllPublished().Count();
|
||||
|
||||
// TO (Option A - use existing QueryOperationService methods):
|
||||
var result = GetPublishedCount(); // Create helper using CountPublished()
|
||||
|
||||
// TO (Option B - inline query):
|
||||
using var scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
var result = DocumentRepository.Count(QueryNotTrashed);
|
||||
```
|
||||
```
|
||||
|
||||
### 2.2 DeleteLocked Implementations Differ in Safety Bounds
|
||||
|
||||
**Description:** The plan correctly identifies the need to unify DeleteLocked (Task 4 Step 5a), but the implementations have important differences:
|
||||
|
||||
| Aspect | ContentService.DeleteLocked | ContentCrudService.DeleteLocked | ContentMoveOperationService.DeleteLocked |
|
||||
|--------|----------------------------|--------------------------------|------------------------------------------|
|
||||
| Iteration bounds | ❌ None (`while (total > 0)`) | ✅ `maxIterations = 10000` | ✅ `MaxDeleteIterations = 10000` |
|
||||
| Empty batch detection | ❌ None | ✅ Logs warning | ✅ Logs warning |
|
||||
| Logging | ❌ None | ✅ Yes | ✅ Yes |
|
||||
|
||||
**Why it matters:** The ContentService version lacks safety bounds and could loop infinitely if the descendant query returns incorrect totals. When DeleteOfTypes delegates to CrudService.DeleteLocked, it will gain these safety features - which is good, but this is a behavioral change that should be documented and tested.
|
||||
|
||||
**Actionable Fix:** Update Task 4 Step 5a to explicitly document this behavioral improvement:
|
||||
|
||||
```markdown
|
||||
### Step 5a: Verify DeleteLocked constant values match (v5.0 addition)
|
||||
|
||||
[existing content...]
|
||||
|
||||
**Behavioral change note:** The ContentService.DeleteLocked implementation lacks:
|
||||
- Iteration bounds (infinite loop protection)
|
||||
- Empty batch detection with logging
|
||||
- Warning logs for data inconsistencies
|
||||
|
||||
Switching to ContentCrudService.DeleteLocked IMPROVES safety. This is intentional.
|
||||
Add a test to verify the iteration bound behavior:
|
||||
|
||||
```csharp
|
||||
[Test]
|
||||
public void DeleteLocked_WithIterationBound_DoesNotInfiniteLoop()
|
||||
{
|
||||
// Test that deletion completes within MaxDeleteIterations
|
||||
// even if there's a data inconsistency
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
### 2.3 Missing Using Statement for ICoreScope in IContentCrudService
|
||||
|
||||
**Description:** Task 4 Step 1 adds `DeleteLocked(ICoreScope scope, ...)` to IContentCrudService, but doesn't verify the using statement exists.
|
||||
|
||||
**Why it matters:** If `ICoreScope` isn't imported in the interface file, compilation will fail.
|
||||
|
||||
**Actionable Fix:** Add verification step:
|
||||
```markdown
|
||||
### Step 1a: Verify ICoreScope import
|
||||
|
||||
Check that IContentCrudService.cs has the required using:
|
||||
```bash
|
||||
grep -n "using Umbraco.Cms.Core.Scoping" src/Umbraco.Core/Services/IContentCrudService.cs
|
||||
```
|
||||
|
||||
If missing, add:
|
||||
```csharp
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
```
|
||||
```
|
||||
|
||||
### 2.4 ContentService.DeleteLocked Uses Different Descendant Query
|
||||
|
||||
**Description:** The ContentService.DeleteLocked calls `GetPagedDescendants` (the public method), while ContentCrudService.DeleteLocked calls `GetPagedDescendantsLocked`:
|
||||
|
||||
```csharp
|
||||
// ContentService.cs:840
|
||||
IEnumerable<IContent> descendants = GetPagedDescendants(content.Id, 0, pageSize, out total, ...);
|
||||
|
||||
// ContentCrudService.cs:653
|
||||
IEnumerable<IContent> descendants = GetPagedDescendantsLocked(content.Id, 0, pageSize, out total, ...);
|
||||
```
|
||||
|
||||
**Why it matters:** The public `GetPagedDescendants` acquires its own scope/lock, while `GetPagedDescendantsLocked` is already within a write lock. When ContentService.DeleteOfTypes switches to CrudService.DeleteLocked, the locking behavior changes - but this should be correct since DeleteOfTypes already holds a write lock (line 1172).
|
||||
|
||||
**Actionable Fix:** Add verification note to Task 4:
|
||||
```markdown
|
||||
### Step 5b: Verify locking compatibility
|
||||
|
||||
The ContentService.DeleteOfTypes method already holds a write lock:
|
||||
```csharp
|
||||
scope.WriteLock(Constants.Locks.ContentTree); // line 1172
|
||||
```
|
||||
|
||||
Verify that `CrudService.DeleteLocked` uses the locked variant internally (`GetPagedDescendantsLocked`) which expects an existing lock. This is already the case in ContentCrudService.DeleteLocked.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Minor Issues & Improvements
|
||||
|
||||
### 3.1 Task 1 Step 3 Lazy Field List May Be Incomplete
|
||||
|
||||
**Description:** The plan lists 6 Lazy fields to remove but doesn't handle the null assignments in the main constructor (lines 182, 187, 192, 197, 202, 207):
|
||||
|
||||
```csharp
|
||||
_queryOperationServiceLazy = null; // Not needed when directly injected
|
||||
```
|
||||
|
||||
**Suggestion:** After removing the Lazy fields, these null assignments become dead code and should also be removed. Add to Task 1 Step 3:
|
||||
```markdown
|
||||
Also remove the null assignment lines in the main constructor:
|
||||
- `_queryOperationServiceLazy = null;`
|
||||
- `_versionOperationServiceLazy = null;`
|
||||
- `_moveOperationServiceLazy = null;`
|
||||
- `_publishOperationServiceLazy = null;`
|
||||
- `_permissionManagerLazy = null;`
|
||||
- `_blueprintManagerLazy = null;`
|
||||
```
|
||||
|
||||
### 3.2 Task 2 Constructor Parameter Order
|
||||
|
||||
**Description:** Task 2 Step 5 shows the new constructor with `crudService` as a non-lazy parameter, but it's still wrapped in `Lazy<>` in the constructor body (line 341 of the example). This is inconsistent.
|
||||
|
||||
**Suggestion:** Either:
|
||||
1. Keep `_crudServiceLazy` as-is (already works) and document why
|
||||
2. Or convert to direct field like other services and update `CrudService` property
|
||||
|
||||
Current approach in the plan mixes patterns. Clarify which is intended:
|
||||
```markdown
|
||||
**Note:** `_crudServiceLazy` is kept as Lazy<> for historical reasons even though the dependency
|
||||
is directly injected. This could be simplified in a future cleanup but is not in scope for Phase 8.
|
||||
```
|
||||
|
||||
### 3.3 Task 3 Step 4 Tuple Destructuring Note
|
||||
|
||||
**Description:** The plan correctly shows the new tuple element names (`x.Content`, `x.OriginalPath`), but code using `x.Item1`/`x.Item2` would still compile due to tuple compatibility. Consider adding a note that both work but the named version is preferred for clarity.
|
||||
|
||||
### 3.4 Test Verification Missing for Integration Tests
|
||||
|
||||
**Description:** Task 8 Step 2 runs `--filter "FullyQualifiedName~ContentService"` but this may not catch:
|
||||
- `DeliveryApiContentIndexHelperTests` (uses `GetAllPublished`)
|
||||
- Other tests that use ContentService indirectly
|
||||
|
||||
**Suggestion:** Add additional test runs:
|
||||
```bash
|
||||
# Test that uses GetAllPublished
|
||||
dotnet test tests/Umbraco.Tests.Integration --filter "FullyQualifiedName~DeliveryApiContentIndexHelper"
|
||||
```
|
||||
|
||||
### 3.5 Line Count Calculation Documentation
|
||||
|
||||
**Description:** The ~990 target is well-documented in v4.0, but actual results may vary. Add a tolerance note:
|
||||
|
||||
```markdown
|
||||
**Expected: ~990 lines** (±50 lines acceptable due to formatting and optional cleanup decisions)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Questions for Clarification
|
||||
|
||||
### Q1: Should `_crudServiceLazy` be converted to direct field?
|
||||
|
||||
The other six service fields were converted from Lazy to direct injection, but `_crudServiceLazy` remains Lazy. Is this intentional for backward compatibility, or should it be unified in Phase 8?
|
||||
|
||||
**Recommendation:** Keep as-is for Phase 8, document for future cleanup.
|
||||
|
||||
### Q2: What should happen to `_queryNotTrashed` if `GetAllPublished` is kept?
|
||||
|
||||
If GetAllPublished must remain (due to test usage), then `_queryNotTrashed` must also remain. The plan links their removal together but doesn't handle the case where GetAllPublished cannot be removed.
|
||||
|
||||
**Recommendation:** Add conditional logic to Task 6:
|
||||
```markdown
|
||||
If GetAllPublished cannot be removed due to external usage:
|
||||
- Keep `_queryNotTrashed` field
|
||||
- Keep `QueryNotTrashed` property
|
||||
- Document that this is legacy infrastructure for internal/test use
|
||||
```
|
||||
|
||||
### Q3: Version Check Criteria for v19
|
||||
|
||||
Task 1 Step 1 says to verify if removal is acceptable "if current version is v19 or if early removal is approved." What is the current version? How should implementers determine if early removal is approved?
|
||||
|
||||
**Recommendation:** Add explicit version check command:
|
||||
```bash
|
||||
# Check current version target
|
||||
grep -r "Version>" Directory.Build.props | head -5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Final Recommendation
|
||||
|
||||
**Approve with Changes**
|
||||
|
||||
The plan is mature (v5.0) and addresses most critical issues from previous reviews. However, the following changes are required before implementation:
|
||||
|
||||
### Required Changes (Blockers)
|
||||
|
||||
1. **Update Task 6 Step 1** to handle the `DeliveryApiContentIndexHelperTests.cs` usage of `GetAllPublished()` - either keep the method or refactor the test
|
||||
2. **Add Task 4 Step 1a** to verify `ICoreScope` using statement in `IContentCrudService.cs`
|
||||
3. **Update Task 8** to add test coverage for the specific test file using `GetAllPublished`
|
||||
|
||||
### Recommended Changes (Non-Blocking)
|
||||
|
||||
1. Document the safety improvement when switching from ContentService.DeleteLocked to ContentCrudService.DeleteLocked
|
||||
2. Add null assignment cleanup to Task 1 Step 3
|
||||
3. Add explicit version check guidance to Task 1 Step 1
|
||||
4. Add tolerance range to line count expectation in Task 7
|
||||
|
||||
---
|
||||
|
||||
## Summary of Changes for v6.0
|
||||
|
||||
| Section | Issue | Required Change |
|
||||
|---------|-------|-----------------|
|
||||
| 2.1 | GetAllPublished used by tests | Add Step 1b to Task 6 handling test usage |
|
||||
| 2.2 | DeleteLocked safety bounds differ | Document as intentional behavioral improvement |
|
||||
| 2.3 | Missing ICoreScope import | Add Step 1a to Task 4 for using statement verification |
|
||||
| 2.4 | Different descendant query methods | Add Step 5b verification note |
|
||||
| 3.1 | Incomplete Lazy field cleanup | Add null assignment removal to Task 1 Step 3 |
|
||||
| 3.4 | Missing specific test coverage | Add DeliveryApiContentIndexHelper test to Task 8 |
|
||||
|
||||
---
|
||||
|
||||
**End of Critical Review 5**
|
||||
@@ -0,0 +1,343 @@
|
||||
# Critical Implementation Review: Phase 8 Facade Finalization (v6.0)
|
||||
|
||||
**Reviewer Role:** Senior Staff Software Engineer / Strict Code Reviewer
|
||||
**Review Date:** 2025-12-24
|
||||
**Document Reviewed:** `docs/plans/2025-12-24-contentservice-refactor-phase8-implementation.md` v6.0
|
||||
|
||||
---
|
||||
|
||||
## 1. Overall Assessment
|
||||
|
||||
**Summary:** This is a mature, well-iterated implementation plan (v6.0) that has benefited significantly from five previous review cycles. The plan demonstrates clear task decomposition, explicit verification steps, and comprehensive risk mitigation. The reordering of tasks (obsolete constructors first) is a sound optimization that reduces merge conflicts.
|
||||
|
||||
**Strengths:**
|
||||
- Excellent version history and traceability of changes
|
||||
- Clear execution order rationale with dependency analysis
|
||||
- Verification steps after each modification (build, test)
|
||||
- Explicit handling of edge cases discovered in prior reviews
|
||||
- Proper commit message formatting with conventional commits
|
||||
|
||||
**Major Concerns:**
|
||||
1. The `_crudServiceLazy` wrapping in the main constructor is redundant after obsolete constructor removal
|
||||
2. Missing null-check for `content` parameter in the newly public `DeleteLocked` interface method
|
||||
3. Task 6 Step 1b test refactoring is under-specified and may break test assertions
|
||||
4. Potential race condition in `PerformMoveLockedInternal` if accessed concurrently
|
||||
|
||||
---
|
||||
|
||||
## 2. Critical Issues
|
||||
|
||||
### 2.1 Redundant Lazy<> Wrapper in Main Constructor (Performance/Clarity)
|
||||
|
||||
**Location:** Task 2 Step 5, lines 356-358
|
||||
|
||||
**Problem:** The plan retains this pattern in the main constructor:
|
||||
```csharp
|
||||
ArgumentNullException.ThrowIfNull(crudService);
|
||||
_crudServiceLazy = new Lazy<IContentCrudService>(() => crudService);
|
||||
```
|
||||
|
||||
After removing obsolete constructors, there's no reason to wrap an already-injected service in `Lazy<>`. The service is already resolved and passed in—wrapping it just adds indirection.
|
||||
|
||||
**Impact:**
|
||||
- Minor performance overhead (Lazy wrapper allocation)
|
||||
- Code clarity issue (suggests lazy initialization where none exists)
|
||||
- Inconsistent with other services that use direct assignment
|
||||
|
||||
**Fix:**
|
||||
```csharp
|
||||
// Change field declaration:
|
||||
private readonly IContentCrudService _crudService;
|
||||
|
||||
// Change property:
|
||||
private IContentCrudService CrudService => _crudService;
|
||||
|
||||
// Change constructor assignment:
|
||||
ArgumentNullException.ThrowIfNull(crudService);
|
||||
_crudService = crudService;
|
||||
```
|
||||
|
||||
**Severity:** Medium
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Missing Parameter Validation in DeleteLocked Interface Method
|
||||
|
||||
**Location:** Task 4 Steps 1-2
|
||||
|
||||
**Problem:** The `DeleteLocked` method is being promoted from private to public interface method without adding parameter validation:
|
||||
```csharp
|
||||
void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs);
|
||||
```
|
||||
|
||||
Currently, the private implementation likely assumes non-null parameters. Once public, external callers may pass null.
|
||||
|
||||
**Impact:**
|
||||
- Potential `NullReferenceException` if null passed
|
||||
- Violation of defensive programming for public interface methods
|
||||
|
||||
**Fix:** Add to Task 4 Step 2 - verify the implementation includes:
|
||||
```csharp
|
||||
public void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scope);
|
||||
ArgumentNullException.ThrowIfNull(content);
|
||||
ArgumentNullException.ThrowIfNull(evtMsgs);
|
||||
// ... existing implementation
|
||||
}
|
||||
```
|
||||
|
||||
Or document that these checks already exist in the implementation.
|
||||
|
||||
**Severity:** High (public interface contract issue)
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Test Refactoring in Task 6 Step 1b May Break Assertions
|
||||
|
||||
**Location:** Task 6 Step 1b
|
||||
|
||||
**Problem:** The proposed refactoring:
|
||||
```csharp
|
||||
// TO (use repository query):
|
||||
private int GetExpectedNumberOfContentItems()
|
||||
{
|
||||
using var scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
|
||||
var query = ScopeAccessor.AmbientScope?.SqlContext.Query<IContent>()
|
||||
.Where(x => x.Published && !x.Trashed);
|
||||
return DocumentRepository.Count(query);
|
||||
}
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
1. `ScopeAccessor.AmbientScope?.SqlContext` may return null (the `?` operator), leading to `query` being null and `Count(null)` behavior is undefined
|
||||
2. The test class may not have direct access to `ScopeProvider`, `ScopeAccessor`, or `DocumentRepository` - these are infrastructure services
|
||||
3. The original method calls `ContentService.GetAllPublished()` which may have different semantics than a raw repository count
|
||||
|
||||
**Impact:** Test may fail or produce incorrect results after refactoring.
|
||||
|
||||
**Fix:** Use the simpler alternative already mentioned in the plan:
|
||||
```csharp
|
||||
// Simpler approach using existing service:
|
||||
private int GetExpectedNumberOfContentItems()
|
||||
{
|
||||
return ContentQueryOperationService.CountPublished();
|
||||
}
|
||||
```
|
||||
|
||||
Or if `ContentQueryOperationService` is not available in the test base class, inject it:
|
||||
```csharp
|
||||
protected IContentQueryOperationService ContentQueryOperationService => GetRequiredService<IContentQueryOperationService>();
|
||||
```
|
||||
|
||||
Also add a verification step to check that `CountPublished()` returns the same value as the original `GetAllPublished().Count()` before removing the latter.
|
||||
|
||||
**Severity:** High (test breakage risk)
|
||||
|
||||
---
|
||||
|
||||
### 2.4 PerformMoveLocked Thread Safety Concern
|
||||
|
||||
**Location:** Task 3 Step 2
|
||||
|
||||
**Problem:** The new public wrapper method creates a local list and passes it to the internal recursive method:
|
||||
```csharp
|
||||
public IReadOnlyCollection<(IContent Content, string OriginalPath)> PerformMoveLocked(...)
|
||||
{
|
||||
var moves = new List<(IContent, string)>();
|
||||
PerformMoveLockedInternal(content, parentId, parent, userId, moves, trash);
|
||||
return moves.AsReadOnly();
|
||||
}
|
||||
```
|
||||
|
||||
The internal method mutates `moves` recursively. If called concurrently by multiple threads, the list mutation is not thread-safe.
|
||||
|
||||
**Impact:**
|
||||
- Race condition if `MoveToRecycleBin` is called concurrently
|
||||
- Potential data corruption in the moves list
|
||||
|
||||
**Mitigation:** The plan mentions this is used within scope locks (`scope.WriteLock(Constants.Locks.ContentTree)`), which should serialize access. However, this should be explicitly documented:
|
||||
|
||||
**Fix:** Add XML documentation to the interface method:
|
||||
```csharp
|
||||
/// <remarks>
|
||||
/// This method must be called within a scope that holds a write lock on
|
||||
/// <see cref="Constants.Locks.ContentTree"/>. It is not thread-safe.
|
||||
/// </remarks>
|
||||
```
|
||||
|
||||
**Severity:** Medium (mitigated by existing locking pattern, but should be documented)
|
||||
|
||||
---
|
||||
|
||||
### 2.5 EventMessages Nullability in DeleteLocked
|
||||
|
||||
**Location:** Task 4 Step 1
|
||||
|
||||
**Problem:** The interface signature:
|
||||
```csharp
|
||||
void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs);
|
||||
```
|
||||
|
||||
Does not allow `evtMsgs` to be null, but some callers may have nullable event messages. Need to verify all call sites have non-null EventMessages.
|
||||
|
||||
**Fix:** Add verification step in Task 4:
|
||||
```bash
|
||||
# Check how callers obtain EventMessages:
|
||||
grep -B5 "DeleteLocked" src/Umbraco.Core/Services/ContentService.cs src/Umbraco.Core/Services/ContentMoveOperationService.cs
|
||||
```
|
||||
|
||||
Verify callers use `EventMessagesFactory.Get()` which returns non-null.
|
||||
|
||||
**Severity:** Medium
|
||||
|
||||
---
|
||||
|
||||
## 3. Minor Issues & Improvements
|
||||
|
||||
### 3.1 Inconsistent Field Removal Count
|
||||
|
||||
**Location:** Task 2 Step 9 commit message
|
||||
|
||||
The commit message says "9 unused fields" but Step 3 lists 9 fields. This is correct but should be double-checked against actual implementation.
|
||||
|
||||
**Verification:** Count the fields in Step 4:
|
||||
- `_documentBlueprintRepository`
|
||||
- `_propertyValidationService`
|
||||
- `_cultureImpactFactory`
|
||||
- `_propertyEditorCollection`
|
||||
- `_contentSettings`
|
||||
- `_relationService`
|
||||
- `_entityRepository`
|
||||
- `_languageRepository`
|
||||
- `_shortStringHelper`
|
||||
|
||||
Count: 9 fields. **Confirmed correct.**
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Missing IShortStringHelper DI Registration Update
|
||||
|
||||
**Location:** Task 5 Step 4
|
||||
|
||||
The plan says: "Since it uses `AddUnique<IContentCrudService, ContentCrudService>()`, DI should auto-resolve the new dependency."
|
||||
|
||||
**Concern:** This is true for typical DI but should be verified. If `ContentCrudService` is registered via factory lambda (like `ContentService` is in Task 2 Step 6), auto-resolution won't work.
|
||||
|
||||
**Fix:** Add explicit verification:
|
||||
```bash
|
||||
grep -A10 "IContentCrudService" src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
|
||||
```
|
||||
|
||||
If factory registration exists, update it to include `IShortStringHelper`.
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Line Count Verification Should Include Tolerance Rationale
|
||||
|
||||
**Location:** Task 7 Step 1
|
||||
|
||||
The ±50 line tolerance is reasonable but the rationale could be clearer:
|
||||
- If significantly under 940 lines: May have accidentally removed needed code
|
||||
- If significantly over 1040 lines: Cleanup was incomplete
|
||||
|
||||
**Improvement:** Add bounds check logic:
|
||||
```bash
|
||||
lines=$(wc -l < src/Umbraco.Core/Services/ContentService.cs)
|
||||
if [ "$lines" -lt 940 ] || [ "$lines" -gt 1040 ]; then
|
||||
echo "WARNING: Line count $lines outside expected range 940-1040"
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Missing Git Tag Push
|
||||
|
||||
**Location:** Task 8 Step 5
|
||||
|
||||
The plan creates a tag but doesn't push it:
|
||||
```bash
|
||||
git tag -a phase-8-facade-finalization -m "..."
|
||||
```
|
||||
|
||||
**Improvement:** Note that tag needs to be pushed separately if remote sharing is needed:
|
||||
```bash
|
||||
# Local tag created. Push with: git push origin phase-8-facade-finalization
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Task 8 Step 2b Test Coverage Is Optional
|
||||
|
||||
**Location:** Task 8 Step 2b
|
||||
|
||||
The step says "Create or update unit tests" but doesn't mark this as mandatory. Given the interface changes, these tests should be required, not optional.
|
||||
|
||||
**Fix:** Change heading to include "(REQUIRED)" similar to other critical steps.
|
||||
|
||||
---
|
||||
|
||||
## 4. Questions for Clarification
|
||||
|
||||
### Q1: Does ContentCrudService.DeleteLocked Create Its Own Scope?
|
||||
|
||||
The plan assumes `ContentCrudService.DeleteLocked` operates within the caller's scope. Verify:
|
||||
```bash
|
||||
grep -A20 "void DeleteLocked" src/Umbraco.Core/Services/ContentCrudService.cs | head -25
|
||||
```
|
||||
|
||||
If it creates its own scope, this would cause nested transaction issues.
|
||||
|
||||
### Q2: What Happens If DeleteLocked Hits maxIterations?
|
||||
|
||||
The plan mentions iteration bounds (10000) but doesn't specify the behavior when exceeded:
|
||||
- Does it throw an exception?
|
||||
- Does it log and return (partial deletion)?
|
||||
- Is there any data consistency concern?
|
||||
|
||||
This behavior should be documented in the interface XML docs.
|
||||
|
||||
### Q3: Is There an IContentQueryOperationService in Test Base?
|
||||
|
||||
The alternative test refactoring assumes `ContentQueryOperationService.CountPublished()` is available. Verify the integration test base class provides this service.
|
||||
|
||||
### Q4: Are There Other Internal Callers of GetAllPublished?
|
||||
|
||||
The plan checks src/ and tests/ but should also verify:
|
||||
```bash
|
||||
grep -rn "GetAllPublished" . --include="*.cs" | grep -v "ContentService.cs" | grep -v ".git"
|
||||
```
|
||||
|
||||
This ensures no callers in tools/, templates/, or other directories are missed.
|
||||
|
||||
---
|
||||
|
||||
## 5. Final Recommendation
|
||||
|
||||
**Recommendation:** Approve with changes
|
||||
|
||||
The plan is well-structured and has been thoroughly refined over six versions. The following changes are required before implementation:
|
||||
|
||||
### Required Changes
|
||||
|
||||
1. **Remove redundant Lazy wrapper for `_crudService`** (Section 2.1) - Convert to direct field assignment since the service is already injected
|
||||
|
||||
2. **Add parameter validation verification for `DeleteLocked`** (Section 2.2) - Either add null checks or document that they exist
|
||||
|
||||
3. **Fix test refactoring approach** (Section 2.3) - Use `ContentQueryOperationService.CountPublished()` instead of raw repository query
|
||||
|
||||
4. **Add thread-safety documentation** (Section 2.4) - Document the locking requirement on `PerformMoveLocked`
|
||||
|
||||
### Recommended Improvements
|
||||
|
||||
5. Add explicit DI registration verification for `IShortStringHelper` in `ContentCrudService`
|
||||
6. Make Task 8 Step 2b test coverage mandatory instead of optional
|
||||
7. Document `maxIterations` exceeded behavior in interface XML docs
|
||||
|
||||
Once these changes are incorporated, the plan should be ready for execution.
|
||||
|
||||
---
|
||||
|
||||
**End of Critical Review 6**
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Version:** 4.0 (Updated based on Critical Review 3)
|
||||
**Version:** 6.0 (Updated based on Critical Review 5)
|
||||
**Last Updated:** 2025-12-24
|
||||
**Change Summary:** Fixed Task 4 Step 6 (IContentCrudService already exists); corrected line count target (~990 lines); added _contentSettings verification step; changed PerformMoveLocked to return collection (Option A); added unit tests for new interface methods; added API project checks; use method signatures instead of line numbers; resolved _queryNotTrashed disposition.
|
||||
**Change Summary:** Added Task 6 Step 1b (test refactoring for GetAllPublished - CRITICAL); added Task 4 Step 1a (ICoreScope import verification); added Task 8 Step 2b (DeliveryApiContentIndexHelper test coverage); enhanced Task 4 Step 5a with behavioral change documentation; added null assignment cleanup to Task 1 Step 3; added version check command to Task 1 Step 1; added tolerance range to Task 7 line count.
|
||||
|
||||
---
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
| 2.0 | 2025-12-24 | Updated based on Critical Review 1 - see change summary |
|
||||
| 3.0 | 2025-12-24 | Updated based on Critical Review 2 - reordered tasks, unified DeleteLocked, explicit constructor steps |
|
||||
| 4.0 | 2025-12-24 | Updated based on Critical Review 3 - fixed Task 4 Step 6, corrected line count, PerformMoveLocked returns collection |
|
||||
| 5.0 | 2025-12-24 | Updated based on Critical Review 4 - added DeleteOfTypes update step (CRITICAL), internal caller update step, constant verification, enhanced MoveToRecycleBin pattern, clarified non-lazy fields, GetAllPublished fallback |
|
||||
| 6.0 | 2025-12-24 | Updated based on Critical Review 5 - test refactoring for GetAllPublished (CRITICAL), ICoreScope import verification, behavioral change documentation, null assignment cleanup, version check command, line count tolerance |
|
||||
|
||||
---
|
||||
|
||||
@@ -125,8 +127,13 @@ All CRUD, Query, Version, Move, Publish, Permission, and Blueprint methods are a
|
||||
|
||||
The obsolete constructors are marked "Scheduled removal in v19". Verify this is acceptable:
|
||||
|
||||
1. Check if current version is v19 or if early removal is approved
|
||||
2. Review the obsolete message text
|
||||
1. Check the current version target:
|
||||
```bash
|
||||
grep -r "Version>" Directory.Build.props | head -5
|
||||
```
|
||||
2. If version is v19 or later, removal is approved
|
||||
3. If version is earlier, check with team lead or skip this task
|
||||
4. Review the obsolete message text to confirm scheduled removal version
|
||||
|
||||
If removal is NOT approved for current version, **skip this task** and keep obsolete constructors.
|
||||
|
||||
@@ -145,7 +152,7 @@ grep -n 'Obsolete.*Scheduled removal in v19' src/Umbraco.Core/Services/ContentSe
|
||||
|
||||
Delete both constructors entirely, including their `[Obsolete]` attributes and method bodies.
|
||||
|
||||
### Step 3: Remove Lazy field declarations (v3.0 explicit list)
|
||||
### Step 3: Remove Lazy field declarations (v3.0 explicit list, v5.0 clarification, v6.0 null assignments)
|
||||
|
||||
Remove these Lazy fields that are no longer needed:
|
||||
- `_queryOperationServiceLazy`
|
||||
@@ -157,6 +164,19 @@ Remove these Lazy fields that are no longer needed:
|
||||
|
||||
**Note:** Keep `_crudServiceLazy` - it is used by the main constructor.
|
||||
|
||||
**Clarification (v5.0):** Keep the non-lazy versions (`_queryOperationService`, `_versionOperationService`, `_moveOperationService`, `_publishOperationService`, `_permissionManager`, `_blueprintManager`) as they are populated by the main constructor. Only the Lazy variants are removed.
|
||||
|
||||
**v6.0 Addition:** Also remove the null assignment lines in the main constructor that become dead code after removing the Lazy fields:
|
||||
```csharp
|
||||
// Remove these lines from the main constructor:
|
||||
_queryOperationServiceLazy = null;
|
||||
_versionOperationServiceLazy = null;
|
||||
_moveOperationServiceLazy = null;
|
||||
_publishOperationServiceLazy = null;
|
||||
_permissionManagerLazy = null;
|
||||
_blueprintManagerLazy = null;
|
||||
```
|
||||
|
||||
### Step 4: Simplify service accessor properties
|
||||
|
||||
Update the service accessor properties to remove null checks for lazy fields:
|
||||
@@ -476,25 +496,99 @@ private void PerformMoveLockedInternal(IContent content, int parentId, IContent?
|
||||
|
||||
This keeps the internal recursive implementation unchanged while providing a clean public interface.
|
||||
|
||||
### Step 2a: Update internal callers to use renamed method (v5.0 addition)
|
||||
|
||||
After renaming to `PerformMoveLockedInternal`, update all internal call sites within `ContentMoveOperationService`:
|
||||
|
||||
1. Run grep to find all internal callers:
|
||||
```bash
|
||||
grep -n "PerformMoveLocked" src/Umbraco.Core/Services/ContentMoveOperationService.cs
|
||||
```
|
||||
|
||||
2. In the `Move()` method, update the call:
|
||||
```csharp
|
||||
// FROM:
|
||||
PerformMoveLocked(content, parentId, parent, userId, moves, trash);
|
||||
// TO:
|
||||
PerformMoveLockedInternal(content, parentId, parent, userId, moves, trash);
|
||||
```
|
||||
|
||||
3. Verify no other internal callers remain pointing to the old method name.
|
||||
|
||||
### Step 3: Verify build compiles
|
||||
|
||||
Run: `dotnet build src/Umbraco.Core/Umbraco.Core.csproj`
|
||||
Expected: Build succeeds
|
||||
|
||||
### Step 4: Update ContentService.MoveToRecycleBin to use service (v4.0 - new return value)
|
||||
### Step 4: Update ContentService.MoveToRecycleBin to use service (v4.0 - new return value, v5.0 enhanced)
|
||||
|
||||
Replace the `PerformMoveLocked` call in ContentService with delegation. The new signature returns the collection:
|
||||
Replace the `PerformMoveLocked` call in ContentService with delegation. The new signature returns the collection.
|
||||
|
||||
**Complete update pattern (v5.0):**
|
||||
|
||||
```csharp
|
||||
// In MoveToRecycleBin, replace:
|
||||
// var moves = new List<(IContent, string)>();
|
||||
// PerformMoveLocked(content, Constants.System.RecycleBinContent, null, userId, moves, true);
|
||||
// With:
|
||||
// In MoveToRecycleBin, find this pattern (lines ~907-918):
|
||||
// OLD:
|
||||
var moves = new List<(IContent, string)>();
|
||||
PerformMoveLocked(content, Constants.System.RecycleBinContent, null, userId, moves, true);
|
||||
scope.Notifications.Publish(new ContentTreeChangeNotification(...));
|
||||
|
||||
MoveToRecycleBinEventInfo<IContent>[] moveInfo = moves
|
||||
.Select(x => new MoveToRecycleBinEventInfo<IContent>(x.Item1, x.Item2))
|
||||
.ToArray();
|
||||
|
||||
// NEW:
|
||||
var moves = MoveOperationService.PerformMoveLocked(content, Constants.System.RecycleBinContent, null, userId, true);
|
||||
scope.Notifications.Publish(new ContentTreeChangeNotification(...));
|
||||
|
||||
// The rest of the code using 'moves' works as-is since tuple destructuring is compatible
|
||||
// (x.Item1/x.Item2 or x.Content/x.OriginalPath both work with named tuples)
|
||||
MoveToRecycleBinEventInfo<IContent>[] moveInfo = moves
|
||||
.Select(x => new MoveToRecycleBinEventInfo<IContent>(x.Content, x.OriginalPath))
|
||||
.ToArray();
|
||||
```
|
||||
|
||||
The caller no longer needs to create the collection - it's returned by the service.
|
||||
|
||||
### Step 4a: Update ContentService.DeleteOfTypes to use new signatures (v5.0 addition - CRITICAL)
|
||||
|
||||
The `DeleteOfTypes` method also calls both `PerformMoveLocked` and `DeleteLocked`. **Without this update, the build will fail after removing the local methods.**
|
||||
|
||||
1. First, locate the `DeleteOfTypes` method in ContentService (lines ~1154-1231).
|
||||
|
||||
2. Find the loop that calls `PerformMoveLocked` (around line 1207):
|
||||
```csharp
|
||||
// OLD:
|
||||
foreach (IContent child in children)
|
||||
{
|
||||
PerformMoveLocked(child, Constants.System.RecycleBinContent, null, userId, moves, true);
|
||||
}
|
||||
|
||||
// NEW:
|
||||
foreach (IContent child in children)
|
||||
{
|
||||
var childMoves = MoveOperationService.PerformMoveLocked(child, Constants.System.RecycleBinContent, null, userId, true);
|
||||
foreach (var move in childMoves)
|
||||
{
|
||||
moves.Add(move); // Aggregate into the overall moves list
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Find the `DeleteLocked` call (around line 1213):
|
||||
```csharp
|
||||
// OLD:
|
||||
DeleteLocked(scope, content, eventMessages);
|
||||
|
||||
// NEW:
|
||||
CrudService.DeleteLocked(scope, content, eventMessages);
|
||||
```
|
||||
|
||||
4. Ensure the `moves` list is still properly initialized before the loop:
|
||||
```csharp
|
||||
var moves = new List<(IContent Content, string OriginalPath)>();
|
||||
```
|
||||
|
||||
### Step 5: Remove duplicate methods from ContentService
|
||||
|
||||
Delete these methods from ContentService.cs:
|
||||
@@ -559,6 +653,21 @@ EOF
|
||||
void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs);
|
||||
```
|
||||
|
||||
### Step 1a: Verify ICoreScope import (v6.0 addition)
|
||||
|
||||
The `DeleteLocked` signature uses `ICoreScope`. Verify the using statement exists in `IContentCrudService.cs`:
|
||||
|
||||
```bash
|
||||
grep -n "using Umbraco.Cms.Core.Scoping" src/Umbraco.Core/Services/IContentCrudService.cs
|
||||
```
|
||||
|
||||
If missing, add the using statement at the top of the file:
|
||||
```csharp
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
```
|
||||
|
||||
**Note:** Without this import, the interface will fail to compile with an error about `ICoreScope` being undefined.
|
||||
|
||||
### Step 2: Change existing method visibility in ContentCrudService
|
||||
|
||||
Change the existing private method to public:
|
||||
@@ -598,6 +707,51 @@ CrudService.DeleteLocked(scope, content, eventMessages);
|
||||
|
||||
Delete the `DeleteLocked` method (lines ~825-848).
|
||||
|
||||
### Step 5a: Verify DeleteLocked constant values match (v5.0 addition, v6.0 behavioral note)
|
||||
|
||||
Before unification, verify both implementations use equivalent values to avoid behavioral changes:
|
||||
|
||||
```bash
|
||||
# Check ContentCrudService constants
|
||||
grep -n "pageSize = \|maxIterations = " src/Umbraco.Core/Services/ContentCrudService.cs
|
||||
|
||||
# Check ContentMoveOperationService constants
|
||||
grep -n "MaxDeleteIterations\|DefaultPageSize" src/Umbraco.Core/Services/ContentMoveOperationService.cs
|
||||
```
|
||||
|
||||
**Expected:** `pageSize = 500` and `maxIterations = 10000` in both implementations.
|
||||
|
||||
**If values differ:**
|
||||
1. Document the change in the commit message
|
||||
2. Update tests if behavior changes
|
||||
3. Consider which values are correct and whether to update ContentCrudService to match
|
||||
|
||||
### Step 5b: Verify locking compatibility (v6.0 addition)
|
||||
|
||||
The ContentService.DeleteOfTypes method already holds a write lock:
|
||||
```csharp
|
||||
scope.WriteLock(Constants.Locks.ContentTree); // line 1172
|
||||
```
|
||||
|
||||
Verify that `CrudService.DeleteLocked` uses the locked variant internally (`GetPagedDescendantsLocked`) which expects an existing lock. This is already the case in ContentCrudService.DeleteLocked.
|
||||
|
||||
**Behavioral change note (v6.0):** The ContentService.DeleteLocked implementation lacks:
|
||||
- Iteration bounds (infinite loop protection)
|
||||
- Empty batch detection with logging
|
||||
- Warning logs for data inconsistencies
|
||||
|
||||
Switching to ContentCrudService.DeleteLocked **IMPROVES safety**. This is intentional and beneficial.
|
||||
|
||||
Consider adding a test to verify the iteration bound behavior:
|
||||
```csharp
|
||||
[Test]
|
||||
public void DeleteLocked_WithIterationBound_DoesNotInfiniteLoop()
|
||||
{
|
||||
// Test that deletion completes within MaxDeleteIterations
|
||||
// even if there's a data inconsistency
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Unify ContentMoveOperationService.EmptyRecycleBin (v4.0 corrected)
|
||||
|
||||
ContentMoveOperationService **already has** `IContentCrudService` as a constructor parameter (assigned to `_crudService` field). Update `EmptyRecycleBin` to call `IContentCrudService.DeleteLocked` instead of its own local `DeleteLocked` method:
|
||||
@@ -763,10 +917,18 @@ EOF
|
||||
**Files:**
|
||||
- Modify: `src/Umbraco.Core/Services/ContentService.cs`
|
||||
|
||||
### Step 1: Check GetAllPublished usage (v4.0 expanded)
|
||||
### Step 1: Check GetAllPublished usage (v4.0 expanded, v5.0 fallback)
|
||||
|
||||
This method is `internal` (not public). Check if it's called externally:
|
||||
|
||||
**First, verify if GetAllPublished exists (v5.0):**
|
||||
|
||||
Run: `grep -n "GetAllPublished" src/Umbraco.Core/Services/ContentService.cs`
|
||||
|
||||
**If no matches found:** The method has already been removed. Proceed to verify `_queryNotTrashed` usage only and skip to Step 2.
|
||||
|
||||
**If matches found**, continue with the usage check:
|
||||
|
||||
Run: `grep -rn "GetAllPublished" src/ --include="*.cs" | grep -v ContentService.cs`
|
||||
|
||||
**Note (v2.0):** Internal methods can be used by test projects via `InternalsVisibleTo`. Check test projects too:
|
||||
@@ -781,7 +943,48 @@ Run: `grep -rn "GetAllPublished" src/Umbraco.Infrastructure/ src/Umbraco.Web.Com
|
||||
|
||||
Run: `grep -rn "GetAllPublished" src/Umbraco.Cms.Api.Management/ src/Umbraco.Cms.Api.Delivery/ --include="*.cs"`
|
||||
|
||||
If not used externally, in tests, or in API projects, remove it along with `_queryNotTrashed` field. If used, keep both.
|
||||
If not used externally, in tests, or in API projects, remove it along with `_queryNotTrashed` field. If used, see Step 1b.
|
||||
|
||||
### Step 1b: Refactor test usage of GetAllPublished (v6.0 addition - CRITICAL)
|
||||
|
||||
**Known usage:** `tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexHelperTests.cs:116`
|
||||
|
||||
The test method `GetExpectedNumberOfContentItems()` calls `ContentService.GetAllPublished().Count()`.
|
||||
|
||||
**Refactor the test to use repository directly:**
|
||||
|
||||
```csharp
|
||||
// File: tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexHelperTests.cs
|
||||
// Method: GetExpectedNumberOfContentItems()
|
||||
|
||||
// FROM:
|
||||
var result = ContentService.GetAllPublished().Count();
|
||||
|
||||
// TO (use repository query):
|
||||
private int GetExpectedNumberOfContentItems()
|
||||
{
|
||||
using var scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
|
||||
// Query for published content count using repository
|
||||
var query = ScopeAccessor.AmbientScope?.SqlContext.Query<IContent>()
|
||||
.Where(x => x.Published && !x.Trashed);
|
||||
return DocumentRepository.Count(query);
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative if repository access is complex:**
|
||||
```csharp
|
||||
// Use IContentQueryOperationService if available in test base
|
||||
var publishedCount = ContentQueryOperationService.CountPublished();
|
||||
```
|
||||
|
||||
After refactoring, verify the test still passes:
|
||||
```bash
|
||||
dotnet test tests/Umbraco.Tests.Integration --filter "FullyQualifiedName~DeliveryApiContentIndexHelper"
|
||||
```
|
||||
|
||||
**Once the test is refactored**, `GetAllPublished` and `_queryNotTrashed` can be safely removed from ContentService.
|
||||
|
||||
### Step 2: Remove HasUnsavedChanges if unused
|
||||
|
||||
@@ -835,11 +1038,16 @@ EOF
|
||||
**Files:**
|
||||
- Review: `src/Umbraco.Core/Services/ContentService.cs`
|
||||
|
||||
### Step 1: Count lines (v4.0 corrected)
|
||||
### Step 1: Count lines (v4.0 corrected, v6.0 tolerance)
|
||||
|
||||
Run: `wc -l src/Umbraco.Core/Services/ContentService.cs`
|
||||
|
||||
**Expected: ~990 lines** (calculated from 1330 - ~340 lines of removal)
|
||||
**Expected: ~990 lines (±50 lines acceptable)** - calculated from 1330 - ~340 lines of removal
|
||||
|
||||
The tolerance accounts for:
|
||||
- Formatting variations
|
||||
- Optional cleanup decisions (comments, blank lines)
|
||||
- Minor differences in removed method lengths
|
||||
|
||||
Line removal breakdown:
|
||||
- Obsolete constructors: ~160 lines
|
||||
@@ -895,7 +1103,17 @@ Expected: All 15+ tests pass
|
||||
Run: `dotnet test tests/Umbraco.Tests.Integration --filter "FullyQualifiedName~ContentService"`
|
||||
Expected: All tests pass
|
||||
|
||||
### Step 2a: Add unit tests for newly exposed interface methods (v4.0 addition)
|
||||
### Step 2a: Run DeliveryApiContentIndexHelper tests (v6.0 addition)
|
||||
|
||||
This test file was modified in Task 6 Step 1b. Verify the refactored test still passes:
|
||||
|
||||
```bash
|
||||
dotnet test tests/Umbraco.Tests.Integration --filter "FullyQualifiedName~DeliveryApiContentIndexHelper"
|
||||
```
|
||||
|
||||
Expected: All tests pass, confirming the GetAllPublished refactoring didn't break functionality.
|
||||
|
||||
### Step 2b: Add unit tests for newly exposed interface methods (v4.0 addition, renumbered v6.0)
|
||||
|
||||
Create or update unit tests to cover the newly public interface methods:
|
||||
|
||||
@@ -1002,20 +1220,20 @@ EOF
|
||||
|
||||
## Summary
|
||||
|
||||
### Tasks Overview (v4.0 - Updated)
|
||||
### Tasks Overview (v6.0 - Updated)
|
||||
|
||||
| Task | Description | Est. Steps | v4.0 Changes |
|
||||
| Task | Description | Est. Steps | v6.0 Changes |
|
||||
|------|-------------|------------|--------------|
|
||||
| 1 | Remove Obsolete Constructors | 7 | Use method signatures instead of line numbers |
|
||||
| 2 | Remove Unused Fields and Simplify Constructor | 10 | **Added** Step 1a: _contentSettings verification |
|
||||
| 3 | Expose PerformMoveLocked on interface | 7 | **Changed** to return collection (Option A - clean API) |
|
||||
| 4 | Expose DeleteLocked + Unify Implementations | 8 | **Fixed** Step 6: IContentCrudService already exists |
|
||||
| 1 | Remove Obsolete Constructors | 7 | **Added** version check command, **Added** null assignment cleanup |
|
||||
| 2 | Remove Unused Fields and Simplify Constructor | 10 | No change |
|
||||
| 3 | Expose PerformMoveLocked on interface | 9 | No change from v5.0 |
|
||||
| 4 | Expose DeleteLocked + Unify Implementations | 11 | **Added** Step 1a (ICoreScope import), **Added** Step 5b (locking/behavioral note) |
|
||||
| 5 | Extract CheckDataIntegrity to ContentCrudService | 8 | No change |
|
||||
| 6 | Clean Up Remaining Internal Methods | 7 | **Added** API project checks |
|
||||
| 7 | Verify Line Count and Final Cleanup | 4 | **Fixed** target: ~990 lines |
|
||||
| 8 | Run Full Test Suite (Phase Gate) | 6 | **Added** Step 2a: unit tests for new interface methods |
|
||||
| 9 | Update Design Document | 4 | Updated line count references |
|
||||
| **Total** | | **61** | |
|
||||
| 6 | Clean Up Remaining Internal Methods | 8 | **Added** Step 1b (test refactoring - CRITICAL) |
|
||||
| 7 | Verify Line Count and Final Cleanup | 4 | **Added** ±50 line tolerance |
|
||||
| 8 | Run Full Test Suite (Phase Gate) | 7 | **Added** Step 2a (DeliveryApiContentIndexHelper tests) |
|
||||
| 9 | Update Design Document | 4 | No change |
|
||||
| **Total** | | **68** | |
|
||||
|
||||
### Expected Outcomes
|
||||
|
||||
@@ -1098,4 +1316,36 @@ This version addresses all findings from Critical Review 3:
|
||||
|
||||
---
|
||||
|
||||
**End of Phase 8 Implementation Plan v4.0**
|
||||
## Review Feedback Incorporated (v5.0)
|
||||
|
||||
This version addresses all findings from Critical Review 4:
|
||||
|
||||
| Review Finding | Section | Resolution |
|
||||
|----------------|---------|------------|
|
||||
| **CRITICAL:** DeleteOfTypes method uses both PerformMoveLocked and DeleteLocked but not updated | 2.3 | **Added** Task 3 Step 4a - explicit step to update DeleteOfTypes method |
|
||||
| PerformMoveLocked internal caller updates missing | 2.1 | **Added** Task 3 Step 2a - update internal callers after renaming to PerformMoveLockedInternal |
|
||||
| Missing DeleteLocked constant value verification | 2.2 | **Added** Task 4 Step 5a - verify pageSize/maxIterations match before unification |
|
||||
| MoveToRecycleBin update pattern incomplete | 3.1 | **Enhanced** Task 3 Step 4 - show complete code update pattern with tuple element names |
|
||||
| Lazy field list clarification needed | 3.2 | **Clarified** Task 1 Step 3 - explicitly note that non-lazy versions are kept |
|
||||
| GetAllPublished may not exist | 3.3 | **Added** fallback handling in Task 6 Step 1 - check existence first |
|
||||
|
||||
---
|
||||
|
||||
## Review Feedback Incorporated (v6.0)
|
||||
|
||||
This version addresses all findings from Critical Review 5:
|
||||
|
||||
| Review Finding | Section | Resolution |
|
||||
|----------------|---------|------------|
|
||||
| **CRITICAL:** GetAllPublished used by integration tests | 2.1 | **Added** Task 6 Step 1b - refactor DeliveryApiContentIndexHelperTests to use repository query instead |
|
||||
| Missing ICoreScope import verification | 2.3 | **Added** Task 4 Step 1a - verify using statement exists in IContentCrudService.cs |
|
||||
| DeleteLocked safety bounds differ (undocumented improvement) | 2.2 | **Enhanced** Task 4 Step 5a - documented behavioral improvement (iteration bounds, logging) |
|
||||
| Different descendant query methods | 2.4 | **Added** Task 4 Step 5b - verify locking compatibility |
|
||||
| Incomplete Lazy field cleanup (null assignments) | 3.1 | **Added** null assignment removal to Task 1 Step 3 |
|
||||
| Missing specific test coverage for DeliveryApiContentIndexHelper | 3.4 | **Added** Task 8 Step 2a - run DeliveryApiContentIndexHelper tests |
|
||||
| Line count target needs tolerance | 3.5 | **Added** ±50 lines tolerance to Task 7 Step 1 |
|
||||
| Missing version check command | Q3 | **Added** explicit grep command to Task 1 Step 1 |
|
||||
|
||||
---
|
||||
|
||||
**End of Phase 8 Implementation Plan v6.0**
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
# Phase 8 Task 1 Code Quality Review: Remove Obsolete Constructors
|
||||
|
||||
**Reviewer:** Senior Code Reviewer
|
||||
**Date:** 2025-12-24
|
||||
**Commit Range:** cacbbf3ca8..aa7e19e608
|
||||
**Task:** Remove obsolete constructors from ContentService and simplify accessor properties
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Assessment:** ✅ **APPROVED**
|
||||
|
||||
The implementation successfully completes Task 1 objectives with high code quality. All obsolete constructors have been removed, Lazy fields eliminated, and service accessor properties simplified. The code compiles without errors and follows established patterns.
|
||||
|
||||
**Key Metrics:**
|
||||
- Constructors removed: 2 (166 lines)
|
||||
- Lazy fields removed: 6 declarations
|
||||
- Null assignments removed: 6 lines
|
||||
- Service accessor properties simplified: 6
|
||||
- Build status: ✅ Success (warnings only, no errors)
|
||||
- Current line count: 1128 (baseline for Phase 8)
|
||||
|
||||
---
|
||||
|
||||
## What Was Done Well
|
||||
|
||||
### 1. Complete Obsolete Constructor Removal ✅
|
||||
|
||||
Both obsolete constructors marked for v19 removal were completely removed:
|
||||
- Constructor with `IAuditRepository` parameter (legacy signature)
|
||||
- Constructor with `IAuditService` but without Phase 2-7 services (intermediate signature)
|
||||
|
||||
**File:** `/home/yv01p/Umbraco-CMS/src/Umbraco.Core/Services/ContentService.cs`
|
||||
|
||||
This eliminates approximately 166 lines of legacy code including:
|
||||
- Method signatures and attributes
|
||||
- Field assignments
|
||||
- StaticServiceProvider lazy resolution logic
|
||||
- All 7 Lazy field instantiations per constructor
|
||||
|
||||
### 2. Lazy Field Cleanup ✅
|
||||
|
||||
All 6 Lazy field declarations were removed correctly:
|
||||
```csharp
|
||||
// Removed:
|
||||
- _queryOperationServiceLazy
|
||||
- _versionOperationServiceLazy
|
||||
- _moveOperationServiceLazy
|
||||
- _publishOperationServiceLazy
|
||||
- _permissionManagerLazy
|
||||
- _blueprintManagerLazy
|
||||
```
|
||||
|
||||
**Correctly preserved:** `_crudServiceLazy` which is still used by the main constructor.
|
||||
|
||||
### 3. Null Assignment Cleanup ✅
|
||||
|
||||
Removed 6 dead code lines from the main constructor (lines that assigned `null` to removed Lazy fields):
|
||||
```csharp
|
||||
// Removed dead code like:
|
||||
_queryOperationServiceLazy = null; // etc.
|
||||
```
|
||||
|
||||
This was a crucial detail from plan v6.0 Step 3 that could have been missed.
|
||||
|
||||
### 4. Service Accessor Property Simplification ✅
|
||||
|
||||
All 6 service accessor properties were correctly simplified from dual-check pattern to single-check:
|
||||
|
||||
**Before:**
|
||||
```csharp
|
||||
private IContentQueryOperationService QueryOperationService =>
|
||||
_queryOperationService ?? _queryOperationServiceLazy?.Value
|
||||
?? throw new InvalidOperationException("QueryOperationService not initialized. Ensure the service is properly injected via constructor.");
|
||||
```
|
||||
|
||||
**After:**
|
||||
```csharp
|
||||
private IContentQueryOperationService QueryOperationService =>
|
||||
_queryOperationService ?? throw new InvalidOperationException("QueryOperationService not initialized.");
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Simpler null-coalescing logic
|
||||
- Clearer error messages (removed verbose "Ensure..." text)
|
||||
- No lazy fallback path needed
|
||||
- Better performance (one field check vs. two)
|
||||
|
||||
### 5. Documentation Cleanup ✅
|
||||
|
||||
Removed verbose XML comments from the simplified accessor properties. The properties are now private implementation details with self-documenting names and clear exception messages.
|
||||
|
||||
### 6. Pattern Consistency ✅
|
||||
|
||||
The implementation maintains consistency across all 6 affected service accessors:
|
||||
- `QueryOperationService`
|
||||
- `VersionOperationService`
|
||||
- `MoveOperationService`
|
||||
- `PublishOperationService`
|
||||
- `PermissionManager`
|
||||
- `BlueprintManager`
|
||||
|
||||
All follow the identical pattern, which is maintainable and clear.
|
||||
|
||||
---
|
||||
|
||||
## Issues Found
|
||||
|
||||
### Critical Issues
|
||||
|
||||
**None identified.** ✅
|
||||
|
||||
### Important Issues
|
||||
|
||||
**None identified.** ✅
|
||||
|
||||
### Minor Issues / Suggestions
|
||||
|
||||
#### 1. Many Unused Fields Still Remain (Planning Issue, Not Implementation)
|
||||
|
||||
**File:** `/home/yv01p/Umbraco-CMS/src/Umbraco.Core/Services/ContentService.cs:33-48`
|
||||
|
||||
The following fields are still declared but marked for removal in Task 2:
|
||||
- `_documentBlueprintRepository` (line 35)
|
||||
- `_entityRepository` (line 37)
|
||||
- `_languageRepository` (line 38)
|
||||
- `_propertyValidationService` (line 40)
|
||||
- `_shortStringHelper` (line 41)
|
||||
- `_cultureImpactFactory` (line 42)
|
||||
- `_propertyEditorCollection` (line 44)
|
||||
- `_contentSettings` (line 46)
|
||||
- `_relationService` (line 47)
|
||||
|
||||
**Assessment:** This is intentional per the implementation plan. Task 1 only removes obsolete constructors, while Task 2 handles unused field removal. This is a staged approach to minimize risk.
|
||||
|
||||
**Recommendation:** No action needed. This is by design.
|
||||
|
||||
#### 2. Constructor Still Has 23 Parameters (Planning Context)
|
||||
|
||||
**File:** `/home/yv01p/Umbraco-CMS/src/Umbraco.Core/Services/ContentService.cs:93-117`
|
||||
|
||||
The main constructor still has 23 parameters, many of which will be removed in Task 2.
|
||||
|
||||
**Assessment:** This is intentional. Task 1 focuses on removing backward-compatibility constructors, not simplifying the main constructor. Task 2 will reduce this to ~15 parameters.
|
||||
|
||||
**Recommendation:** No action needed. Follow the plan sequence.
|
||||
|
||||
---
|
||||
|
||||
## Code Quality Assessment
|
||||
|
||||
### Adherence to Patterns ✅
|
||||
|
||||
The implementation follows established Umbraco patterns:
|
||||
|
||||
1. **Service Accessor Pattern:** Private properties with null-coalescing and clear exceptions
|
||||
2. **Direct Injection Pattern:** Services injected via constructor, not lazily resolved
|
||||
3. **ArgumentNullException Pattern:** Validates injected services with `ArgumentNullException.ThrowIfNull`
|
||||
4. **Breaking Change Documentation:** Obsolete attributes with clear removal version
|
||||
|
||||
### Error Handling ✅
|
||||
|
||||
All service accessor properties throw clear `InvalidOperationException` with service name:
|
||||
```csharp
|
||||
throw new InvalidOperationException("QueryOperationService not initialized.");
|
||||
```
|
||||
|
||||
This provides immediate clarity if DI is misconfigured.
|
||||
|
||||
### Performance ✅
|
||||
|
||||
Removing lazy resolution paths improves performance:
|
||||
- No lazy field checks
|
||||
- No `Lazy<T>.Value` overhead
|
||||
- Direct field access only
|
||||
|
||||
### Maintainability ✅
|
||||
|
||||
The code is more maintainable after this change:
|
||||
- Fewer code paths (no lazy fallback)
|
||||
- Simpler logic (one null check vs. two)
|
||||
- Less coupling (no StaticServiceProvider dependency)
|
||||
- Clearer intent (direct injection only)
|
||||
|
||||
### Testing Impact ✅
|
||||
|
||||
The changes maintain backward compatibility for:
|
||||
- Public API surface (no changes to public methods)
|
||||
- Service behavior (only constructor signatures changed)
|
||||
- Notification ordering (unchanged)
|
||||
|
||||
**Test Status:** Build succeeded. Tests should pass (will be verified in Task 1 Step 6).
|
||||
|
||||
---
|
||||
|
||||
## Plan Alignment Analysis
|
||||
|
||||
### Task 1 Requirements vs. Implementation
|
||||
|
||||
| Requirement | Status | Evidence |
|
||||
|------------|--------|----------|
|
||||
| Remove 2 obsolete constructors | ✅ Complete | Both constructors removed (git diff lines 185-363) |
|
||||
| Remove 6 Lazy field declarations | ✅ Complete | All 6 removed (lines 58-69 in diff) |
|
||||
| Keep `_crudServiceLazy` | ✅ Complete | Field preserved at line 49 |
|
||||
| Remove null assignment lines | ✅ Complete | 6 lines removed from main constructor |
|
||||
| Simplify 6 accessor properties | ✅ Complete | All 6 updated (lines 72-88 in diff) |
|
||||
| Run build verification | ✅ Complete | Build succeeded with 0 errors |
|
||||
| Tests verification | ⏳ Pending | Step 6 not executed yet |
|
||||
| Commit with message | ⏳ Pending | Step 7 not executed yet |
|
||||
|
||||
### Deviations from Plan
|
||||
|
||||
**None.** The implementation precisely follows the plan steps.
|
||||
|
||||
---
|
||||
|
||||
## Architecture and Design Review
|
||||
|
||||
### SOLID Principles ✅
|
||||
|
||||
1. **Single Responsibility:** ContentService remains a facade, dependency resolution logic removed
|
||||
2. **Open/Closed:** Service injection supports extension via DI
|
||||
3. **Liskov Substitution:** N/A (no inheritance changes)
|
||||
4. **Interface Segregation:** N/A (no interface changes)
|
||||
5. **Dependency Inversion:** Improved - all dependencies now injected directly
|
||||
|
||||
### Separation of Concerns ✅
|
||||
|
||||
The removal of `StaticServiceProvider` usage improves separation:
|
||||
- DI container responsibility (service resolution) removed from ContentService
|
||||
- Constructor responsibility (validation and assignment) clearly defined
|
||||
- No service locator anti-pattern
|
||||
|
||||
### Coupling Reduction ✅
|
||||
|
||||
Dependencies removed:
|
||||
- `StaticServiceProvider` (no more static service locator calls)
|
||||
- Lazy field abstractions (simpler direct field access)
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
**No security implications.** This is an internal refactoring with no changes to:
|
||||
- Authentication/authorization logic
|
||||
- Data validation
|
||||
- Input sanitization
|
||||
- Access control
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Improvements ✅
|
||||
|
||||
1. **Constructor execution:** Removed 7 `Lazy<T>` instantiations per obsolete constructor
|
||||
2. **Service access:** Removed lazy value resolution checks (2 null checks → 1 null check)
|
||||
3. **Memory:** Reduced object allocation (no Lazy wrappers for 6 services)
|
||||
|
||||
### No Regressions
|
||||
|
||||
Service accessor performance is identical or better:
|
||||
- Direct field access remains O(1)
|
||||
- Exception path is identical (service not initialized)
|
||||
|
||||
---
|
||||
|
||||
## Documentation and Comments
|
||||
|
||||
### Adequate Documentation ✅
|
||||
|
||||
The code is self-documenting:
|
||||
- Clear service accessor names
|
||||
- Explicit exception messages
|
||||
- ArgumentNullException for validation
|
||||
|
||||
### Removed Excessive Comments ✅
|
||||
|
||||
XML documentation was appropriately removed from private accessor properties. These were verbose and not exposed in IntelliSense.
|
||||
|
||||
**Before:** 4-line XML comment per accessor
|
||||
**After:** No comment (private implementation detail)
|
||||
|
||||
This is correct. Private implementation details should be self-documenting, not over-commented.
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### For Current Implementation
|
||||
|
||||
1. **Proceed to Task 1 Step 6:** Run integration tests
|
||||
```bash
|
||||
dotnet test tests/Umbraco.Tests.Integration --filter "FullyQualifiedName~ContentService"
|
||||
```
|
||||
|
||||
2. **Verify breaking change impact:** Confirm no external code uses obsolete constructors
|
||||
```bash
|
||||
grep -rn "new ContentService" tests/ --include="*.cs" | grep -v "// "
|
||||
```
|
||||
|
||||
3. **Commit with proper message:** Use the commit message from Task 1 Step 7
|
||||
|
||||
### For Subsequent Tasks
|
||||
|
||||
1. **Task 2 (Field Removal):** Will significantly reduce constructor parameters and line count
|
||||
2. **Monitor line count progress:** Track toward ~990 line target
|
||||
3. **Run full integration suite:** After Task 2 completion to verify DI changes
|
||||
|
||||
### For Future Refactoring
|
||||
|
||||
1. **Consider adding a constructor for testing:** Unit tests may benefit from a simpler constructor that doesn't require all 23 dependencies
|
||||
2. **Document breaking changes:** Update CHANGELOG or migration guide for external packages using obsolete constructors
|
||||
3. **Consider analyzer rule:** Add Roslyn analyzer to prevent StaticServiceProvider usage in new code
|
||||
|
||||
---
|
||||
|
||||
## Final Assessment
|
||||
|
||||
### Strengths Summary
|
||||
|
||||
1. ✅ **Complete and accurate implementation** of plan requirements
|
||||
2. ✅ **Clean code** with simplified logic and better performance
|
||||
3. ✅ **Zero compilation errors** - build succeeded
|
||||
4. ✅ **Consistent pattern** across all 6 service accessors
|
||||
5. ✅ **Proper cleanup** including null assignments (easy to miss)
|
||||
6. ✅ **Maintains backward compatibility** for public API
|
||||
|
||||
### Risk Assessment
|
||||
|
||||
**Risk Level:** ⚠️ **Low**
|
||||
|
||||
**Risks:**
|
||||
1. Breaking change for external code using obsolete constructors
|
||||
- **Mitigation:** Constructors marked with `[Obsolete]` for multiple versions
|
||||
- **Impact:** Low - external code should have migrated already
|
||||
|
||||
2. Test failures if tests instantiate ContentService directly
|
||||
- **Mitigation:** Run test suite (Task 1 Step 6)
|
||||
- **Impact:** Low - tests should use DI
|
||||
|
||||
**No regressions expected.** The changes remove dead code paths without altering behavior.
|
||||
|
||||
### Approval Status
|
||||
|
||||
**✅ APPROVED**
|
||||
|
||||
The implementation is high quality, follows the plan precisely, and improves code maintainability. Proceed with:
|
||||
1. Task 1 Step 6 (test verification)
|
||||
2. Task 1 Step 7 (commit)
|
||||
3. Task 2 (unused field removal)
|
||||
|
||||
---
|
||||
|
||||
## Detailed File Changes
|
||||
|
||||
### `/home/yv01p/Umbraco-CMS/src/Umbraco.Core/Services/ContentService.cs`
|
||||
|
||||
**Lines removed:** ~172 lines
|
||||
**Lines modified:** 12 lines (6 accessor properties, 6 field assignments)
|
||||
|
||||
**Changes:**
|
||||
1. **Lines 54-69 (removed):** 6 Lazy field declarations
|
||||
2. **Lines 72-88 (simplified):** 6 service accessor properties
|
||||
3. **Lines 145-165 (removed):** Null assignments in main constructor
|
||||
4. **Lines 185-363 (removed):** 2 obsolete constructors (~178 lines total)
|
||||
|
||||
**Correctness:** ✅ All changes are correct and complete
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Build Output
|
||||
|
||||
```
|
||||
Build Succeeded
|
||||
Warnings: 8603 (pre-existing)
|
||||
Errors: 0 ✅
|
||||
Time: 00:00:22.24
|
||||
```
|
||||
|
||||
**Line Count:** 1128 lines (baseline for Phase 8)
|
||||
|
||||
---
|
||||
|
||||
**Review Complete**
|
||||
**Recommendation:** APPROVED - Proceed to next steps (test verification and commit)
|
||||
Reference in New Issue
Block a user