Compare commits
9 Commits
phase-1-cr
...
phase-2-qu
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bb1b24f92 | |||
| 1bc741b470 | |||
| dc44bebfcc | |||
| fb20c480e3 | |||
| ff4bdb2509 | |||
| 31dfe07aa7 | |||
| cf8394b6fd | |||
| 36d1fcc8ac | |||
| d78238b247 |
@@ -14,6 +14,7 @@
|
|||||||
| 1.3 | Added detailed test strategy (15 tests for coverage gaps) |
|
| 1.3 | Added detailed test strategy (15 tests for coverage gaps) |
|
||||||
| 1.4 | Added phase gates with test execution commands and regression protocol |
|
| 1.4 | Added phase gates with test execution commands and regression protocol |
|
||||||
| 1.5 | Added performance benchmarks (33 tests for baseline comparison) |
|
| 1.5 | Added performance benchmarks (33 tests for baseline comparison) |
|
||||||
|
| 1.6 | Phase 2 complete - QueryOperationService extracted |
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -386,22 +387,29 @@ public class ContentServicesComposer : IComposer
|
|||||||
|
|
||||||
Each phase MUST run tests before and after to verify no regressions.
|
Each phase MUST run tests before and after to verify no regressions.
|
||||||
|
|
||||||
| Phase | Service | Tests to Run | Gate |
|
| Phase | Service | Tests to Run | Gate | Status |
|
||||||
|-------|---------|--------------|------|
|
|-------|---------|--------------|------|--------|
|
||||||
| 0 | Write tests | `ContentServiceRefactoringTests` | All 15 pass |
|
| 0 | Write tests | `ContentServiceRefactoringTests` | All 15 pass | ✅ Complete |
|
||||||
| 1 | CRUD Service | All ContentService*Tests | All pass |
|
| 1 | CRUD Service | All ContentService*Tests | All pass | ✅ Complete |
|
||||||
| 2 | Query Service | All ContentService*Tests | All pass |
|
| 2 | Query Service | All ContentService*Tests | All pass | ✅ Complete |
|
||||||
| 3 | Version Service | All ContentService*Tests | All pass |
|
| 3 | Version Service | All ContentService*Tests | All pass | Pending |
|
||||||
| 4 | Move Service | All ContentService*Tests + Sort/MoveToRecycleBin tests | All pass |
|
| 4 | Move Service | All ContentService*Tests + Sort/MoveToRecycleBin tests | All pass | Pending |
|
||||||
| 5 | Publish Operation Service | All ContentService*Tests + Notification ordering tests | All pass |
|
| 5 | Publish Operation Service | All ContentService*Tests + Notification ordering tests | All pass | Pending |
|
||||||
| 6 | Permission Manager | All ContentService*Tests + Permission tests | All pass |
|
| 6 | Permission Manager | All ContentService*Tests + Permission tests | All pass | Pending |
|
||||||
| 7 | Blueprint Manager | All ContentService*Tests | All pass |
|
| 7 | Blueprint Manager | All ContentService*Tests | All pass | Pending |
|
||||||
| 8 | Facade | **Full test suite** | All pass |
|
| 8 | Facade | **Full test suite** | All pass | Pending |
|
||||||
|
|
||||||
### Phase Details
|
### Phase Details
|
||||||
|
|
||||||
1. **Phase 0: Write Tests** - Create `ContentServiceRefactoringTests.cs` with all 15 tests
|
1. **Phase 0: Write Tests** ✅ - Created `ContentServiceRefactoringTests.cs` with 16 tests (15 original + 1 DI test)
|
||||||
2. **Phase 1: CRUD Service** - Establish patterns, base class
|
2. **Phase 1: CRUD Service** ✅ - Complete! Created:
|
||||||
|
- `ContentServiceBase.cs` - Abstract base class with shared infrastructure
|
||||||
|
- `ContentServiceConstants.cs` - Shared constants
|
||||||
|
- `IContentCrudService.cs` - Interface (21 methods)
|
||||||
|
- `ContentCrudService.cs` - Implementation (~750 lines)
|
||||||
|
- Updated `ContentService.cs` to delegate CRUD operations (reduced from 3823 to 3497 lines)
|
||||||
|
- Benchmark regression enforcement (20% threshold, CI-configurable)
|
||||||
|
- Git tag: `phase-1-crud-extraction`
|
||||||
3. **Phase 2: Query Service** - Read-only operations, low risk
|
3. **Phase 2: Query Service** - Read-only operations, low risk
|
||||||
4. **Phase 3: Version Service** - Straightforward extraction
|
4. **Phase 3: Version Service** - Straightforward extraction
|
||||||
5. **Phase 4: Move Service** - Depends on CRUD; Sort and MoveToRecycleBin tests critical
|
5. **Phase 4: Move Service** - Depends on CRUD; Sort and MoveToRecycleBin tests critical
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
|||||||
Services.AddUnique<IContentPermissionService, ContentPermissionService>();
|
Services.AddUnique<IContentPermissionService, ContentPermissionService>();
|
||||||
Services.AddUnique<IDictionaryPermissionService, DictionaryPermissionService>();
|
Services.AddUnique<IDictionaryPermissionService, DictionaryPermissionService>();
|
||||||
Services.AddUnique<IContentCrudService, ContentCrudService>();
|
Services.AddUnique<IContentCrudService, ContentCrudService>();
|
||||||
|
Services.AddUnique<IContentQueryOperationService, ContentQueryOperationService>();
|
||||||
Services.AddUnique<IContentService>(sp =>
|
Services.AddUnique<IContentService>(sp =>
|
||||||
new ContentService(
|
new ContentService(
|
||||||
sp.GetRequiredService<ICoreScopeProvider>(),
|
sp.GetRequiredService<ICoreScopeProvider>(),
|
||||||
@@ -318,7 +319,8 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
|||||||
sp.GetRequiredService<IIdKeyMap>(),
|
sp.GetRequiredService<IIdKeyMap>(),
|
||||||
sp.GetRequiredService<IOptionsMonitor<ContentSettings>>(),
|
sp.GetRequiredService<IOptionsMonitor<ContentSettings>>(),
|
||||||
sp.GetRequiredService<IRelationService>(),
|
sp.GetRequiredService<IRelationService>(),
|
||||||
sp.GetRequiredService<IContentCrudService>()));
|
sp.GetRequiredService<IContentCrudService>(),
|
||||||
|
sp.GetRequiredService<IContentQueryOperationService>()));
|
||||||
Services.AddUnique<IContentBlueprintEditingService, ContentBlueprintEditingService>();
|
Services.AddUnique<IContentBlueprintEditingService, ContentBlueprintEditingService>();
|
||||||
Services.AddUnique<IContentEditingService, ContentEditingService>();
|
Services.AddUnique<IContentEditingService, ContentEditingService>();
|
||||||
Services.AddUnique<IContentPublishingService, ContentPublishingService>();
|
Services.AddUnique<IContentPublishingService, ContentPublishingService>();
|
||||||
|
|||||||
169
src/Umbraco.Core/Services/ContentQueryOperationService.cs
Normal file
169
src/Umbraco.Core/Services/ContentQueryOperationService.cs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Umbraco.Cms.Core.Events;
|
||||||
|
using Umbraco.Cms.Core.Models;
|
||||||
|
using Umbraco.Cms.Core.Persistence.Querying;
|
||||||
|
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||||
|
using Umbraco.Cms.Core.Scoping;
|
||||||
|
|
||||||
|
namespace Umbraco.Cms.Core.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements content query operations (counting, filtering by type/level).
|
||||||
|
/// </summary>
|
||||||
|
public class ContentQueryOperationService : ContentServiceBase, IContentQueryOperationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default ordering for paged queries.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Ordering DefaultSortOrdering = Ordering.By("sortOrder");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logger for this service (for debugging, performance monitoring, or error tracking).
|
||||||
|
/// </summary>
|
||||||
|
private readonly ILogger<ContentQueryOperationService> _logger;
|
||||||
|
|
||||||
|
public ContentQueryOperationService(
|
||||||
|
ICoreScopeProvider provider,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
IEventMessagesFactory eventMessagesFactory,
|
||||||
|
IDocumentRepository documentRepository,
|
||||||
|
IAuditService auditService,
|
||||||
|
IUserIdKeyResolver userIdKeyResolver)
|
||||||
|
: base(provider, loggerFactory, eventMessagesFactory, documentRepository, auditService, userIdKeyResolver)
|
||||||
|
{
|
||||||
|
_logger = loggerFactory.CreateLogger<ContentQueryOperationService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Count Operations
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int Count(string? contentTypeAlias = null)
|
||||||
|
{
|
||||||
|
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||||
|
scope.ReadLock(Constants.Locks.ContentTree);
|
||||||
|
return DocumentRepository.Count(contentTypeAlias);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CountPublished(string? contentTypeAlias = null)
|
||||||
|
{
|
||||||
|
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||||
|
scope.ReadLock(Constants.Locks.ContentTree);
|
||||||
|
return DocumentRepository.CountPublished(contentTypeAlias);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CountChildren(int parentId, string? contentTypeAlias = null)
|
||||||
|
{
|
||||||
|
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||||
|
scope.ReadLock(Constants.Locks.ContentTree);
|
||||||
|
return DocumentRepository.CountChildren(parentId, contentTypeAlias);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CountDescendants(int parentId, string? contentTypeAlias = null)
|
||||||
|
{
|
||||||
|
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||||
|
scope.ReadLock(Constants.Locks.ContentTree);
|
||||||
|
return DocumentRepository.CountDescendants(parentId, contentTypeAlias);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Hierarchy Queries
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <remarks>
|
||||||
|
/// The returned enumerable may be lazily evaluated. Callers should materialize
|
||||||
|
/// results (e.g., call ToList()) if they need to access them after the scope is disposed.
|
||||||
|
/// This is consistent with the existing ContentService.GetByLevel implementation.
|
||||||
|
/// </remarks>
|
||||||
|
public IEnumerable<IContent> GetByLevel(int level)
|
||||||
|
{
|
||||||
|
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||||
|
scope.ReadLock(Constants.Locks.ContentTree);
|
||||||
|
IQuery<IContent>? query = Query<IContent>().Where(x => x.Level == level && x.Trashed == false);
|
||||||
|
return DocumentRepository.Get(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Paged Type Queries
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<IContent> GetPagedOfType(
|
||||||
|
int contentTypeId,
|
||||||
|
long pageIndex,
|
||||||
|
int pageSize,
|
||||||
|
out long totalRecords,
|
||||||
|
IQuery<IContent>? filter = null,
|
||||||
|
Ordering? ordering = null)
|
||||||
|
{
|
||||||
|
if (pageIndex < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageSize <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
ordering ??= DefaultSortOrdering;
|
||||||
|
|
||||||
|
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||||
|
scope.ReadLock(Constants.Locks.ContentTree);
|
||||||
|
|
||||||
|
// Note: filter=null is valid and means no additional filtering beyond the content type
|
||||||
|
return DocumentRepository.GetPage(
|
||||||
|
Query<IContent>()?.Where(x => x.ContentTypeId == contentTypeId),
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
out totalRecords,
|
||||||
|
filter,
|
||||||
|
ordering);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<IContent> GetPagedOfTypes(
|
||||||
|
int[] contentTypeIds,
|
||||||
|
long pageIndex,
|
||||||
|
int pageSize,
|
||||||
|
out long totalRecords,
|
||||||
|
IQuery<IContent>? filter = null,
|
||||||
|
Ordering? ordering = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(contentTypeIds);
|
||||||
|
|
||||||
|
if (pageIndex < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageSize <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
ordering ??= DefaultSortOrdering;
|
||||||
|
|
||||||
|
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||||
|
|
||||||
|
// Expression trees require a List for Contains() - array not supported.
|
||||||
|
// This O(n) copy is unavoidable but contentTypeIds is typically small.
|
||||||
|
List<int> contentTypeIdsAsList = [.. contentTypeIds];
|
||||||
|
|
||||||
|
scope.ReadLock(Constants.Locks.ContentTree);
|
||||||
|
|
||||||
|
// Note: filter=null is valid and means no additional filtering beyond the content types
|
||||||
|
return DocumentRepository.GetPage(
|
||||||
|
Query<IContent>()?.Where(x => contentTypeIdsAsList.Contains(x.ContentTypeId)),
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
out totalRecords,
|
||||||
|
filter,
|
||||||
|
ordering);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -51,6 +51,18 @@ public class ContentService : RepositoryService, IContentService
|
|||||||
// Property for convenient access (deferred resolution for both paths)
|
// Property for convenient access (deferred resolution for both paths)
|
||||||
private IContentCrudService CrudService => _crudServiceLazy.Value;
|
private IContentCrudService CrudService => _crudServiceLazy.Value;
|
||||||
|
|
||||||
|
// Query operation service fields (for Phase 2 extracted query operations)
|
||||||
|
private readonly IContentQueryOperationService? _queryOperationService;
|
||||||
|
private readonly Lazy<IContentQueryOperationService>? _queryOperationServiceLazy;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the query operation service.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown if the service was not properly initialized.</exception>
|
||||||
|
private IContentQueryOperationService QueryOperationService =>
|
||||||
|
_queryOperationService ?? _queryOperationServiceLazy?.Value
|
||||||
|
?? throw new InvalidOperationException("QueryOperationService not initialized. Ensure the service is properly injected via constructor.");
|
||||||
|
|
||||||
#region Constructors
|
#region Constructors
|
||||||
|
|
||||||
[Microsoft.Extensions.DependencyInjection.ActivatorUtilitiesConstructor]
|
[Microsoft.Extensions.DependencyInjection.ActivatorUtilitiesConstructor]
|
||||||
@@ -72,7 +84,8 @@ public class ContentService : RepositoryService, IContentService
|
|||||||
IIdKeyMap idKeyMap,
|
IIdKeyMap idKeyMap,
|
||||||
IOptionsMonitor<ContentSettings> optionsMonitor,
|
IOptionsMonitor<ContentSettings> optionsMonitor,
|
||||||
IRelationService relationService,
|
IRelationService relationService,
|
||||||
IContentCrudService crudService) // NEW PARAMETER - direct injection
|
IContentCrudService crudService,
|
||||||
|
IContentQueryOperationService queryOperationService) // NEW PARAMETER - Phase 2 query operations
|
||||||
: base(provider, loggerFactory, eventMessagesFactory)
|
: base(provider, loggerFactory, eventMessagesFactory)
|
||||||
{
|
{
|
||||||
_documentRepository = documentRepository;
|
_documentRepository = documentRepository;
|
||||||
@@ -97,6 +110,11 @@ public class ContentService : RepositoryService, IContentService
|
|||||||
ArgumentNullException.ThrowIfNull(crudService);
|
ArgumentNullException.ThrowIfNull(crudService);
|
||||||
// Wrap in Lazy for consistent access pattern (already resolved, so returns immediately)
|
// Wrap in Lazy for consistent access pattern (already resolved, so returns immediately)
|
||||||
_crudServiceLazy = new Lazy<IContentCrudService>(() => crudService);
|
_crudServiceLazy = new Lazy<IContentCrudService>(() => crudService);
|
||||||
|
|
||||||
|
// Phase 2: Query operation service (direct injection)
|
||||||
|
ArgumentNullException.ThrowIfNull(queryOperationService);
|
||||||
|
_queryOperationService = queryOperationService;
|
||||||
|
_queryOperationServiceLazy = null; // Not needed when directly injected
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("Use the non-obsolete constructor instead. Scheduled removal in v19.")]
|
[Obsolete("Use the non-obsolete constructor instead. Scheduled removal in v19.")]
|
||||||
@@ -148,6 +166,11 @@ public class ContentService : RepositoryService, IContentService
|
|||||||
_crudServiceLazy = new Lazy<IContentCrudService>(() =>
|
_crudServiceLazy = new Lazy<IContentCrudService>(() =>
|
||||||
StaticServiceProvider.Instance.GetRequiredService<IContentCrudService>(),
|
StaticServiceProvider.Instance.GetRequiredService<IContentCrudService>(),
|
||||||
LazyThreadSafetyMode.ExecutionAndPublication);
|
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||||
|
|
||||||
|
// Phase 2: Lazy resolution of IContentQueryOperationService
|
||||||
|
_queryOperationServiceLazy = new Lazy<IContentQueryOperationService>(() =>
|
||||||
|
StaticServiceProvider.Instance.GetRequiredService<IContentQueryOperationService>(),
|
||||||
|
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("Use the non-obsolete constructor instead. Scheduled removal in v19.")]
|
[Obsolete("Use the non-obsolete constructor instead. Scheduled removal in v19.")]
|
||||||
@@ -198,6 +221,11 @@ public class ContentService : RepositoryService, IContentService
|
|||||||
_crudServiceLazy = new Lazy<IContentCrudService>(() =>
|
_crudServiceLazy = new Lazy<IContentCrudService>(() =>
|
||||||
StaticServiceProvider.Instance.GetRequiredService<IContentCrudService>(),
|
StaticServiceProvider.Instance.GetRequiredService<IContentCrudService>(),
|
||||||
LazyThreadSafetyMode.ExecutionAndPublication);
|
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||||
|
|
||||||
|
// Phase 2: Lazy resolution of IContentQueryOperationService
|
||||||
|
_queryOperationServiceLazy = new Lazy<IContentQueryOperationService>(() =>
|
||||||
|
StaticServiceProvider.Instance.GetRequiredService<IContentQueryOperationService>(),
|
||||||
|
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -274,40 +302,16 @@ public class ContentService : RepositoryService, IContentService
|
|||||||
#region Count
|
#region Count
|
||||||
|
|
||||||
public int CountPublished(string? contentTypeAlias = null)
|
public int CountPublished(string? contentTypeAlias = null)
|
||||||
{
|
=> QueryOperationService.CountPublished(contentTypeAlias);
|
||||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
||||||
{
|
|
||||||
scope.ReadLock(Constants.Locks.ContentTree);
|
|
||||||
return _documentRepository.CountPublished(contentTypeAlias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Count(string? contentTypeAlias = null)
|
public int Count(string? contentTypeAlias = null)
|
||||||
{
|
=> QueryOperationService.Count(contentTypeAlias);
|
||||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
||||||
{
|
|
||||||
scope.ReadLock(Constants.Locks.ContentTree);
|
|
||||||
return _documentRepository.Count(contentTypeAlias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CountChildren(int parentId, string? contentTypeAlias = null)
|
public int CountChildren(int parentId, string? contentTypeAlias = null)
|
||||||
{
|
=> QueryOperationService.CountChildren(parentId, contentTypeAlias);
|
||||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
||||||
{
|
|
||||||
scope.ReadLock(Constants.Locks.ContentTree);
|
|
||||||
return _documentRepository.CountChildren(parentId, contentTypeAlias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CountDescendants(int parentId, string? contentTypeAlias = null)
|
public int CountDescendants(int parentId, string? contentTypeAlias = null)
|
||||||
{
|
=> QueryOperationService.CountDescendants(parentId, contentTypeAlias);
|
||||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
||||||
{
|
|
||||||
scope.ReadLock(Constants.Locks.ContentTree);
|
|
||||||
return _documentRepository.CountDescendants(parentId, contentTypeAlias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -545,63 +549,11 @@ public class ContentService : RepositoryService, IContentService
|
|||||||
out long totalRecords,
|
out long totalRecords,
|
||||||
IQuery<IContent>? filter = null,
|
IQuery<IContent>? filter = null,
|
||||||
Ordering? ordering = null)
|
Ordering? ordering = null)
|
||||||
{
|
=> QueryOperationService.GetPagedOfType(contentTypeId, pageIndex, pageSize, out totalRecords, filter, ordering);
|
||||||
if (pageIndex < 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageSize <= 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(pageSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
ordering ??= Ordering.By("sortOrder");
|
|
||||||
|
|
||||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
||||||
{
|
|
||||||
scope.ReadLock(Constants.Locks.ContentTree);
|
|
||||||
return _documentRepository.GetPage(
|
|
||||||
Query<IContent>()?.Where(x => x.ContentTypeId == contentTypeId),
|
|
||||||
pageIndex,
|
|
||||||
pageSize,
|
|
||||||
out totalRecords,
|
|
||||||
filter,
|
|
||||||
ordering);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<IContent> GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery<IContent>? filter, Ordering? ordering = null)
|
public IEnumerable<IContent> GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery<IContent>? filter, Ordering? ordering = null)
|
||||||
{
|
=> QueryOperationService.GetPagedOfTypes(contentTypeIds, pageIndex, pageSize, out totalRecords, filter, ordering);
|
||||||
if (pageIndex < 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageSize <= 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(pageSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
ordering ??= Ordering.By("sortOrder");
|
|
||||||
|
|
||||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
||||||
{
|
|
||||||
// Need to use a List here because the expression tree cannot convert the array when used in Contains.
|
|
||||||
// See ExpressionTests.Sql_In().
|
|
||||||
List<int> contentTypeIdsAsList = [.. contentTypeIds];
|
|
||||||
|
|
||||||
scope.ReadLock(Constants.Locks.ContentTree);
|
|
||||||
return _documentRepository.GetPage(
|
|
||||||
Query<IContent>()?.Where(x => contentTypeIdsAsList.Contains(x.ContentTypeId)),
|
|
||||||
pageIndex,
|
|
||||||
pageSize,
|
|
||||||
out totalRecords,
|
|
||||||
filter,
|
|
||||||
ordering);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a collection of <see cref="IContent" /> objects by Level
|
/// Gets a collection of <see cref="IContent" /> objects by Level
|
||||||
@@ -610,14 +562,7 @@ public class ContentService : RepositoryService, IContentService
|
|||||||
/// <returns>An Enumerable list of <see cref="IContent" /> objects</returns>
|
/// <returns>An Enumerable list of <see cref="IContent" /> objects</returns>
|
||||||
/// <remarks>Contrary to most methods, this method filters out trashed content items.</remarks>
|
/// <remarks>Contrary to most methods, this method filters out trashed content items.</remarks>
|
||||||
public IEnumerable<IContent> GetByLevel(int level)
|
public IEnumerable<IContent> GetByLevel(int level)
|
||||||
{
|
=> QueryOperationService.GetByLevel(level);
|
||||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
||||||
{
|
|
||||||
scope.ReadLock(Constants.Locks.ContentTree);
|
|
||||||
IQuery<IContent>? query = Query<IContent>().Where(x => x.Level == level && x.Trashed == false);
|
|
||||||
return _documentRepository.Get(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a specific version of an <see cref="IContent" /> item.
|
/// Gets a specific version of an <see cref="IContent" /> item.
|
||||||
|
|||||||
123
src/Umbraco.Core/Services/IContentQueryOperationService.cs
Normal file
123
src/Umbraco.Core/Services/IContentQueryOperationService.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// src/Umbraco.Core/Services/IContentQueryOperationService.cs
|
||||||
|
using Umbraco.Cms.Core.Models;
|
||||||
|
using Umbraco.Cms.Core.Persistence.Querying;
|
||||||
|
|
||||||
|
namespace Umbraco.Cms.Core.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for content query operations (counting, filtering by type/level).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <strong>Implementation Note:</strong> Do not implement this interface directly.
|
||||||
|
/// Instead, inherit from <see cref="ContentServiceBase"/> which provides required
|
||||||
|
/// infrastructure (scoping, repository access, auditing). Direct implementation
|
||||||
|
/// without this base class will result in missing functionality.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// This interface is part of the ContentService refactoring initiative (Phase 2).
|
||||||
|
/// It extracts query operations into a focused, testable service.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <strong>Versioning Policy:</strong> This interface follows additive-only changes.
|
||||||
|
/// New methods may be added with default implementations. Existing methods will not
|
||||||
|
/// be removed or have signatures changed without a 2 major version deprecation period.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <strong>Version History:</strong>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>v1.0 (Phase 2): Initial interface with Count, GetByLevel, GetPagedOfType operations</description></item>
|
||||||
|
/// </list>
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <since>1.0</since>
|
||||||
|
public interface IContentQueryOperationService : IService
|
||||||
|
{
|
||||||
|
#region Count Operations
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Counts content items, optionally filtered by content type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contentTypeAlias">Optional content type alias to filter by. If the alias doesn't exist, returns 0.</param>
|
||||||
|
/// <returns>The count of matching content items (includes trashed items).</returns>
|
||||||
|
int Count(string? contentTypeAlias = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Counts published content items, optionally filtered by content type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contentTypeAlias">Optional content type alias to filter by. If the alias doesn't exist, returns 0.</param>
|
||||||
|
/// <returns>The count of matching published content items.</returns>
|
||||||
|
int CountPublished(string? contentTypeAlias = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Counts children of a parent, optionally filtered by content type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parentId">The parent content id. If the parent doesn't exist, returns 0.</param>
|
||||||
|
/// <param name="contentTypeAlias">Optional content type alias to filter by. If the alias doesn't exist, returns 0.</param>
|
||||||
|
/// <returns>The count of matching child content items.</returns>
|
||||||
|
int CountChildren(int parentId, string? contentTypeAlias = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Counts descendants of an ancestor, optionally filtered by content type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parentId">The ancestor content id. If the ancestor doesn't exist, returns 0.</param>
|
||||||
|
/// <param name="contentTypeAlias">Optional content type alias to filter by. If the alias doesn't exist, returns 0.</param>
|
||||||
|
/// <returns>The count of matching descendant content items.</returns>
|
||||||
|
int CountDescendants(int parentId, string? contentTypeAlias = null);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Hierarchy Queries
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets content items at a specific tree level.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="level">The tree level (1 = root children, 2 = grandchildren, etc.).</param>
|
||||||
|
/// <returns>Content items at the specified level, excluding trashed items.</returns>
|
||||||
|
IEnumerable<IContent> GetByLevel(int level);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Paged Type Queries
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets paged content items of a specific content type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contentTypeId">The content type id. If the content type doesn't exist, returns empty results with totalRecords = 0.</param>
|
||||||
|
/// <param name="pageIndex">Zero-based page index.</param>
|
||||||
|
/// <param name="pageSize">Page size.</param>
|
||||||
|
/// <param name="totalRecords">Output: total number of matching records.</param>
|
||||||
|
/// <param name="filter">Optional filter query.</param>
|
||||||
|
/// <param name="ordering">Optional ordering (defaults to sortOrder).</param>
|
||||||
|
/// <returns>Paged content items.</returns>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">Thrown when pageIndex is negative or pageSize is less than or equal to zero.</exception>
|
||||||
|
IEnumerable<IContent> GetPagedOfType(
|
||||||
|
int contentTypeId,
|
||||||
|
long pageIndex,
|
||||||
|
int pageSize,
|
||||||
|
out long totalRecords,
|
||||||
|
IQuery<IContent>? filter = null,
|
||||||
|
Ordering? ordering = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets paged content items of multiple content types.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contentTypeIds">The content type ids. If empty or containing non-existent IDs, returns empty results with totalRecords = 0.</param>
|
||||||
|
/// <param name="pageIndex">Zero-based page index.</param>
|
||||||
|
/// <param name="pageSize">Page size.</param>
|
||||||
|
/// <param name="totalRecords">Output: total number of matching records.</param>
|
||||||
|
/// <param name="filter">Optional filter query.</param>
|
||||||
|
/// <param name="ordering">Optional ordering (defaults to sortOrder).</param>
|
||||||
|
/// <returns>Paged content items.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when contentTypeIds is null.</exception>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">Thrown when pageIndex is negative or pageSize is less than or equal to zero.</exception>
|
||||||
|
IEnumerable<IContent> GetPagedOfTypes(
|
||||||
|
int[] contentTypeIds,
|
||||||
|
long pageIndex,
|
||||||
|
int pageSize,
|
||||||
|
out long totalRecords,
|
||||||
|
IQuery<IContent>? filter = null,
|
||||||
|
Ordering? ordering = null);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using Umbraco.Cms.Core.Models;
|
||||||
|
using Umbraco.Cms.Core.Services;
|
||||||
|
using Umbraco.Cms.Tests.Common.Builders;
|
||||||
|
using Umbraco.Cms.Tests.Common.Testing;
|
||||||
|
using Umbraco.Cms.Tests.Integration.Testing;
|
||||||
|
|
||||||
|
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Integration tests for ContentQueryOperationService.
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
[UmbracoTest(
|
||||||
|
Database = UmbracoTestOptions.Database.NewSchemaPerTest,
|
||||||
|
WithApplication = true)]
|
||||||
|
public class ContentQueryOperationServiceTests : UmbracoIntegrationTestWithContent
|
||||||
|
{
|
||||||
|
private IContentQueryOperationService QueryService => GetRequiredService<IContentQueryOperationService>();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Count_WithNoFilter_ReturnsAllContentCount()
|
||||||
|
{
|
||||||
|
// Arrange - base class creates Textpage, Subpage, Subpage2, Subpage3, Trashed
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var count = QueryService.Count();
|
||||||
|
|
||||||
|
// Assert - should return 5 (all items including Trashed)
|
||||||
|
Assert.That(count, Is.EqualTo(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Count_WithNonExistentContentTypeAlias_ReturnsZero()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var nonExistentAlias = "nonexistent-content-type-alias";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var count = QueryService.Count(nonExistentAlias);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(count, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Count_WithContentTypeAlias_ReturnsFilteredCount()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var alias = ContentType.Alias;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var count = QueryService.Count(alias);
|
||||||
|
|
||||||
|
// Assert - all 5 content items use the same content type
|
||||||
|
Assert.That(count, Is.EqualTo(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CountChildren_ReturnsChildCount()
|
||||||
|
{
|
||||||
|
// Arrange - Textpage has children: Subpage, Subpage2, Subpage3
|
||||||
|
var parentId = Textpage.Id;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var count = QueryService.CountChildren(parentId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(count, Is.EqualTo(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetByLevel_ReturnsContentAtLevel()
|
||||||
|
{
|
||||||
|
// Arrange - level 1 is root content
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var items = QueryService.GetByLevel(1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(items, Is.Not.Null);
|
||||||
|
Assert.That(items.All(x => x.Level == 1), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetPagedOfType_ReturnsPaginatedResults()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var contentTypeId = ContentType.Id;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var items = QueryService.GetPagedOfType(contentTypeId, 0, 10, out var total);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(items, Is.Not.Null);
|
||||||
|
Assert.That(total, Is.EqualTo(5)); // All 5 content items are of this type
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetPagedOfTypes_WithEmptyArray_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var items = QueryService.GetPagedOfTypes(Array.Empty<int>(), 0, 10, out var total);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(items, Is.Empty);
|
||||||
|
Assert.That(total, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetPagedOfTypes_WithNonExistentContentTypeIds_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var nonExistentIds = new[] { 999999, 999998 };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var items = QueryService.GetPagedOfTypes(nonExistentIds, 0, 10, out var total);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(items, Is.Empty);
|
||||||
|
Assert.That(total, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CountChildren_WithNonExistentParentId_ReturnsZero()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var nonExistentParentId = 999999;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var count = QueryService.CountChildren(nonExistentParentId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(count, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetByLevel_WithLevelZero_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
// Arrange - level 0 doesn't exist (content starts at level 1)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var items = QueryService.GetByLevel(0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(items, Is.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetByLevel_WithNegativeLevel_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var items = QueryService.GetByLevel(-1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(items, Is.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetPagedOfType_WithNonExistentContentTypeId_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var nonExistentId = 999999;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var items = QueryService.GetPagedOfType(nonExistentId, 0, 10, out var total);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(items, Is.Empty);
|
||||||
|
Assert.That(total, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CountDescendants_ReturnsDescendantCount()
|
||||||
|
{
|
||||||
|
// Arrange - Textpage has descendants: Subpage, Subpage2, Subpage3
|
||||||
|
var ancestorId = Textpage.Id;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var count = QueryService.CountDescendants(ancestorId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(count, Is.EqualTo(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CountDescendants_WithNonExistentAncestorId_ReturnsZero()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var nonExistentId = 999999;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var count = QueryService.CountDescendants(nonExistentId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(count, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CountPublished_WithNoPublishedContent_ReturnsZero()
|
||||||
|
{
|
||||||
|
// Arrange - base class creates content but doesn't publish
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var count = QueryService.CountPublished();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(count, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -673,6 +673,137 @@ internal sealed class ContentServiceRefactoringTests : UmbracoIntegrationTestWit
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Phase 2 - Count Method Delegation Tests
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 2 Test: Verifies Count() via facade returns same result as direct service call.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Count_ViaFacade_ReturnsEquivalentResultToDirectService()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var queryService = GetRequiredService<IContentQueryOperationService>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var facadeCount = ContentService.Count();
|
||||||
|
var directCount = queryService.Count();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(facadeCount, Is.EqualTo(directCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 2 Test: Verifies CountPublished() via facade returns same result as direct service call.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void CountPublished_ViaFacade_ReturnsEquivalentResultToDirectService()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var queryService = GetRequiredService<IContentQueryOperationService>();
|
||||||
|
ContentService.Publish(Textpage, new[] { "*" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var facadeCount = ContentService.CountPublished();
|
||||||
|
var directCount = queryService.CountPublished();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(facadeCount, Is.EqualTo(directCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 2 Test: Verifies CountChildren() via facade returns same result as direct service call.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void CountChildren_ViaFacade_ReturnsEquivalentResultToDirectService()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var queryService = GetRequiredService<IContentQueryOperationService>();
|
||||||
|
var parentId = Textpage.Id;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var facadeCount = ContentService.CountChildren(parentId);
|
||||||
|
var directCount = queryService.CountChildren(parentId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(facadeCount, Is.EqualTo(directCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 2 Test: Verifies CountDescendants() via facade returns same result as direct service call.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void CountDescendants_ViaFacade_ReturnsEquivalentResultToDirectService()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var queryService = GetRequiredService<IContentQueryOperationService>();
|
||||||
|
var parentId = Textpage.Id;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var facadeCount = ContentService.CountDescendants(parentId);
|
||||||
|
var directCount = queryService.CountDescendants(parentId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(facadeCount, Is.EqualTo(directCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 2 Test: Verifies GetByLevel() via facade returns same result as direct service call.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void GetByLevel_ViaFacade_ReturnsEquivalentResultToDirectService()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var queryService = GetRequiredService<IContentQueryOperationService>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var facadeItems = ContentService.GetByLevel(1).ToList();
|
||||||
|
var directItems = queryService.GetByLevel(1).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(facadeItems.Count, Is.EqualTo(directItems.Count));
|
||||||
|
Assert.That(facadeItems.Select(x => x.Id), Is.EquivalentTo(directItems.Select(x => x.Id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 2 Test: Verifies GetPagedOfType() via facade returns same result as direct service call.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void GetPagedOfType_ViaFacade_ReturnsEquivalentResultToDirectService()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var queryService = GetRequiredService<IContentQueryOperationService>();
|
||||||
|
var contentTypeId = ContentType.Id;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var facadeItems = ContentService.GetPagedOfType(contentTypeId, 0, 10, out var facadeTotal).ToList();
|
||||||
|
var directItems = queryService.GetPagedOfType(contentTypeId, 0, 10, out var directTotal).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(facadeTotal, Is.EqualTo(directTotal));
|
||||||
|
Assert.That(facadeItems.Select(x => x.Id), Is.EquivalentTo(directItems.Select(x => x.Id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 2 Test: Verifies GetPagedOfTypes() via facade returns same result as direct service call.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void GetPagedOfTypes_ViaFacade_ReturnsEquivalentResultToDirectService()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var queryService = GetRequiredService<IContentQueryOperationService>();
|
||||||
|
var contentTypeIds = new[] { ContentType.Id };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var facadeItems = ContentService.GetPagedOfTypes(contentTypeIds, 0, 10, out var facadeTotal, null).ToList();
|
||||||
|
var directItems = queryService.GetPagedOfTypes(contentTypeIds, 0, 10, out var directTotal).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(facadeTotal, Is.EqualTo(directTotal));
|
||||||
|
Assert.That(facadeItems.Select(x => x.Id), Is.EquivalentTo(directItems.Select(x => x.Id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Notification handler that tracks the order of notifications for test verification.
|
/// Notification handler that tracks the order of notifications for test verification.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentQueryOperationServiceInterfaceTests.cs
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Umbraco.Cms.Core.Services;
|
||||||
|
|
||||||
|
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class ContentQueryOperationServiceInterfaceTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void IContentQueryOperationService_Interface_Exists()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var interfaceType = typeof(IContentQueryOperationService);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(interfaceType, Is.Not.Null);
|
||||||
|
Assert.That(interfaceType.IsInterface, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IContentQueryOperationService_Extends_IService()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var interfaceType = typeof(IContentQueryOperationService);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.That(typeof(IService).IsAssignableFrom(interfaceType), Is.True);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user