Adds support for the "folders only" flag on retrieving siblings of a node. (#19861)

* Adds support for the "folders only" flag on retrieving siblings of a node.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Updated test code.

* Removed double secondary ordering by node Id and ensured we include this clause for all sort orders.

* Ensure that ordering by node Id is always added only once and last, and only if it's not already been included in the order by clause.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Andy Butland
2025-08-07 12:35:04 +02:00
committed by GitHub
parent eb986d9de9
commit 63ed1eec41
17 changed files with 202 additions and 85 deletions

View File

@@ -15,6 +15,9 @@ public class SiblingsDataTypeTreeController : DataTypeTreeControllerBase
[HttpGet("siblings")]
[ProducesResponseType(typeof(SubsetViewModel<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<SubsetViewModel<DataTypeTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
=> GetSiblings(target, before, after);
public async Task<ActionResult<SubsetViewModel<DataTypeTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetSiblings(target, before, after);
}
}

View File

@@ -36,9 +36,9 @@ public class SiblingsDocumentTreeController : DocumentTreeControllerBase
[HttpGet("siblings")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(SubsetViewModel<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<SubsetViewModel<DocumentTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null)
public async Task<ActionResult<SubsetViewModel<DocumentTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null)
{
IgnoreUserStartNodesForDataType(dataTypeId);
return GetSiblings(target, before, after);
return await GetSiblings(target, before, after);
}
}

View File

@@ -16,10 +16,14 @@ public class SiblingsDocumentBlueprintTreeController : DocumentBlueprintTreeCont
[HttpGet("siblings")]
[ProducesResponseType(typeof(SubsetViewModel<DocumentBlueprintTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<SubsetViewModel<DocumentBlueprintTreeItemResponseModel>>> Siblings(
public async Task<ActionResult<SubsetViewModel<DocumentBlueprintTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after) =>
GetSiblings(target, before, after);
int after,
bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetSiblings(target, before, after);
}
}

View File

@@ -15,10 +15,14 @@ public class SiblingsDocumentTypeTreeController : DocumentTypeTreeControllerBase
[HttpGet("siblings")]
[ProducesResponseType(typeof(SubsetViewModel<DocumentTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<SubsetViewModel<DocumentTypeTreeItemResponseModel>>> Siblings(
public async Task<ActionResult<SubsetViewModel<DocumentTypeTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after) =>
GetSiblings(target, before, after);
int after,
bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetSiblings(target, before, after);
}
}

View File

@@ -25,9 +25,9 @@ public class SiblingsMediaTreeController : MediaTreeControllerBase
[HttpGet("siblings")]
[ProducesResponseType(typeof(SubsetViewModel<MediaTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<SubsetViewModel<MediaTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null)
public async Task<ActionResult<SubsetViewModel<MediaTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null)
{
IgnoreUserStartNodesForDataType(dataTypeId);
return GetSiblings(target, before, after);
return await GetSiblings(target, before, after);
}
}

View File

@@ -15,6 +15,14 @@ public class SiblingsMediaTypeTreeController : MediaTypeTreeControllerBase
[HttpGet("siblings")]
[ProducesResponseType(typeof(SubsetViewModel<MediaTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<SubsetViewModel<MediaTypeTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
=> GetSiblings(target, before, after);
public async Task<ActionResult<SubsetViewModel<MediaTypeTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after,
bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetSiblings(target, before, after);
}
}

View File

@@ -15,10 +15,10 @@ public class SiblingsTemplateTreeController : TemplateTreeControllerBase
[HttpGet("siblings")]
[ProducesResponseType(typeof(SubsetViewModel<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<SubsetViewModel<NamedEntityTreeItemResponseModel>>> Siblings(
public async Task<ActionResult<SubsetViewModel<NamedEntityTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after) =>
GetSiblings(target, before, after);
await GetSiblings(target, before, after);
}

View File

@@ -127,7 +127,7 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
EntityService
.GetSiblings(
target,
ItemObjectType,
[ItemObjectType],
before,
after,
out totalBefore,

View File

@@ -51,6 +51,24 @@ public abstract class FolderTreeControllerBase<TItem> : NamedEntityTreeControlle
take,
out totalItems);
protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter)
{
totalBefore = 0;
totalAfter = 0;
UmbracoObjectTypes[] siblingObjectTypes = GetObjectTypes();
return EntityService.GetSiblings(
target,
siblingObjectTypes,
before,
after,
out totalBefore,
out totalAfter,
ordering: ItemOrdering)
.ToArray();
}
protected override TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity)
{
TItem viewModel = base.MapTreeItemViewModel(parentKey, entity);
@@ -93,19 +111,19 @@ public abstract class FolderTreeControllerBase<TItem> : NamedEntityTreeControlle
{
totalItems = 0;
UmbracoObjectTypes[] childObjectTypes = _foldersOnly ? [FolderObjectType] : [FolderObjectType, ItemObjectType];
UmbracoObjectTypes[] childObjectTypes = GetObjectTypes();
IEntitySlim[] itemEntities = EntityService.GetPagedChildren(
parentKey,
[FolderObjectType, ItemObjectType],
childObjectTypes,
skip,
take,
false,
out totalItems,
ordering: ItemOrdering)
.ToArray();
return itemEntities;
return EntityService.GetPagedChildren(
parentKey,
[FolderObjectType, ItemObjectType],
childObjectTypes,
skip,
take,
false,
out totalItems,
ordering: ItemOrdering)
.ToArray();
}
private UmbracoObjectTypes[] GetObjectTypes() => _foldersOnly ? [FolderObjectType] : [FolderObjectType, ItemObjectType];
}

View File

@@ -190,7 +190,7 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
if (userStartNodePaths.Any(path => $"{targetParent?.Path},".StartsWith($"{path},")))
{
// The requested parent of the target is one of the user start nodes (or a descendant of one), all siblings are by definition allowed.
siblings = _entityService.GetSiblings(targetKey, umbracoObjectType, before, after, out totalBefore, out totalAfter, ordering: ordering).ToArray();
siblings = _entityService.GetSiblings(targetKey, [umbracoObjectType], before, after, out totalBefore, out totalAfter, ordering: ordering).ToArray();
return ChildUserAccessEntities(siblings, userStartNodePaths);
}
@@ -206,7 +206,7 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
// Even though we know the IDs of the allowed sibling entities to fetch, we still use a Query to yield correctly sorted children.
IQuery<IUmbracoEntity> query = _scopeProvider.CreateQuery<IUmbracoEntity>().Where(x => allowedSiblingIds.Contains(x.Id));
siblings = _entityService.GetSiblings(targetKey, umbracoObjectType, before, after, out totalBefore, out totalAfter, query, ordering).ToArray();
siblings = _entityService.GetSiblings(targetKey, [umbracoObjectType], before, after, out totalBefore, out totalAfter, query, ordering).ToArray();
return ChildUserAccessEntities(siblings, userStartNodePaths);
}

View File

@@ -22,7 +22,7 @@ public interface IEntityRepository : IRepository
/// <summary>
/// Gets sibling entities of a specified target entity, within a given range before and after the target, ordered as specified.
/// </summary>
/// <param name="objectType">The object type key of the entities.</param>
/// <param name="objectTypes">The object type keys of the entities.</param>
/// <param name="targetKey">The key of the target entity whose siblings are to be retrieved.</param>
/// <param name="before">The number of siblings to retrieve before the target entity.</param>
/// <param name="after">The number of siblings to retrieve after the target entity.</param>
@@ -32,7 +32,7 @@ public interface IEntityRepository : IRepository
/// <param name="totalAfter">Outputs the total number of siblings after the target entity.</param>
/// <returns>Enumerable of sibling entities.</returns>
IEnumerable<IEntitySlim> GetSiblings(
Guid objectType,
ISet<Guid> objectTypes,
Guid targetKey,
int before,
int after,

View File

@@ -321,7 +321,7 @@ public class EntityService : RepositoryService, IEntityService
/// <inheritdoc />
public IEnumerable<IEntitySlim> GetSiblings(
Guid key,
UmbracoObjectTypes objectType,
IEnumerable<UmbracoObjectTypes> objectTypes,
int before,
int after,
out long totalBefore,
@@ -343,8 +343,10 @@ public class EntityService : RepositoryService, IEntityService
using ICoreScope scope = ScopeProvider.CreateCoreScope();
var objectTypeGuids = objectTypes.Select(x => x.GetGuid()).ToHashSet();
IEnumerable<IEntitySlim> siblings = _entityRepository.GetSiblings(
objectType.GetGuid(),
objectTypeGuids,
key,
before,
after,

View File

@@ -174,7 +174,7 @@ public interface IEntityService
/// Gets sibling entities of a specified target entity, within a given range before and after the target, ordered as specified.
/// </summary>
/// <param name="key">The key of the target entity whose siblings are to be retrieved.</param>
/// <param name="objectType">The object type key of the entities.</param>
/// <param name="objectTypes">The object types of the entities.</param>
/// <param name="before">The number of siblings to retrieve before the target entity. Needs to be greater or equal to 0.</param>
/// <param name="after">The number of siblings to retrieve after the target entity. Needs to be greater or equal to 0.</param>
/// <param name="filter">An optional filter to apply to the result set.</param>
@@ -184,7 +184,7 @@ public interface IEntityService
/// <returns>Enumerable of sibling entities.</returns>
IEnumerable<IEntitySlim> GetSiblings(
Guid key,
UmbracoObjectTypes objectType,
IEnumerable<UmbracoObjectTypes> objectTypes,
int before,
int after,
out long totalBefore,

View File

@@ -95,10 +95,6 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
ApplyOrdering(ref sql, ordering);
}
// TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently
// no matter what we always must have node id ordered at the end
sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId");
// for content we must query for ContentEntityDto entities to produce the correct culture variant entity names
var pageIndexToFetch = pageIndex + 1;
IEnumerable<BaseDto> dtos;
@@ -147,7 +143,7 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
/// <inheritdoc/>
public IEnumerable<IEntitySlim> GetSiblings(
Guid objectType,
ISet<Guid> objectTypes,
Guid targetKey,
int before,
int after,
@@ -167,31 +163,29 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
Sql<ISqlContext> orderingSql = Sql();
ApplyOrdering(ref orderingSql, ordering);
// Get all children of the parent node which is not trashed, ordered by SortOrder, and assign each a row number.
// Get all children of the parent node which are not trashed and match the provided object types.
// Order by SortOrder, and assign each a row number.
// These row numbers are important, we need them to select the "before" and "after" siblings of the target node.
Sql<ISqlContext> rowNumberSql = Sql()
.Select($"ROW_NUMBER() OVER ({orderingSql.SQL}) AS rn")
.AndSelect<NodeDto>(n => n.UniqueId)
.From<NodeDto>()
.Where<NodeDto>(x => x.ParentId == parentId && x.Trashed == false);
.Where<NodeDto>(x => x.ParentId == parentId && x.Trashed == false)
.WhereIn<NodeDto>(x => x.NodeObjectType, objectTypes);
// Apply the filter if provided. Note that in doing this, we'll add more parameters to the query, so need to track
// how many so we can offset the parameter indexes for the "before" and "after" values added later.
int beforeAfterParameterIndexOffset = 0;
// Apply the filter if provided.
if (filter != null)
{
foreach (Tuple<string, object[]> filterClause in filter.GetWhereClauses())
{
rowNumberSql.Where(filterClause.Item1, filterClause.Item2);
// We need to offset by one for each non-array parameter in the filter clause.
// If a query is created using Contains or some other set based operation, we'll get both the array and the
// items in the array provided in the where clauses. It's only the latter that count for applying parameters
// to the SQL statement, and hence we should only offset by them.
beforeAfterParameterIndexOffset += filterClause.Item2.Count(x => !x.GetType().IsArray);
}
}
// By applying additional where clauses with parameters containing an unknown number of elements, the position of the parameters in
// the final query for before and after positions will increase. So we need to calculate the offset based on the provided values.
int beforeAfterParameterIndexOffset = GetBeforeAfterParameterOffset(objectTypes, filter);
// Find the specific row number of the target node.
// We need this to determine the bounds of the row numbers to select.
Sql<ISqlContext> targetRowSql = Sql()
@@ -226,7 +220,31 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
return [];
}
return PerformGetAll(objectType, ordering, sql => sql.WhereIn<NodeDto>(x => x.UniqueId, keys));
// To re-use this method we need to provide a single object type. By convention for folder based trees, we provide the primary object type last.
return PerformGetAll(objectTypes.ToArray(), ordering, sql => sql.WhereIn<NodeDto>(x => x.UniqueId, keys));
}
private static int GetBeforeAfterParameterOffset(ISet<Guid> objectTypes, IQuery<IUmbracoEntity>? filter)
{
int beforeAfterParameterIndexOffset = 0;
// Increment for each object type.
beforeAfterParameterIndexOffset += objectTypes.Count;
// Increment for the provided filter.
if (filter != null)
{
foreach (Tuple<string, object[]> filterClause in filter.GetWhereClauses())
{
// We need to offset by one for each non-array parameter in the filter clause.
// If a query is created using Contains or some other set based operation, we'll get both the array and the
// items in the array provided in the where clauses. It's only the latter that count for applying parameters
// to the SQL statement, and hence we should only offset by them.
beforeAfterParameterIndexOffset += filterClause.Item2.Count(x => !x.GetType().IsArray);
}
}
return beforeAfterParameterIndexOffset;
}
private long GetNumberOfSiblingsOutsideSiblingRange(
@@ -316,16 +334,16 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
}
private IEnumerable<IEntitySlim> PerformGetAll(
Guid objectType,
Guid[] objectTypes,
Ordering ordering,
Action<Sql<ISqlContext>>? filter = null)
{
var isContent = objectType == Constants.ObjectTypes.Document ||
objectType == Constants.ObjectTypes.DocumentBlueprint;
var isMedia = objectType == Constants.ObjectTypes.Media;
var isMember = objectType == Constants.ObjectTypes.Member;
var isContent = objectTypes.Contains(Constants.ObjectTypes.Document) ||
objectTypes.Contains(Constants.ObjectTypes.DocumentBlueprint);
var isMedia = objectTypes.Contains(Constants.ObjectTypes.Media);
var isMember = objectTypes.Contains(Constants.ObjectTypes.Member);
Sql<ISqlContext> sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, ordering, filter);
Sql<ISqlContext> sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypes, ordering, filter);
return GetEntities(sql, isContent, isMedia, isMember);
}
@@ -572,8 +590,17 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
Guid objectType,
Ordering ordering,
Action<Sql<ISqlContext>>? filter)
=> GetFullSqlForEntityType(isContent, isMedia, isMember, [objectType], ordering, filter);
protected Sql<ISqlContext> GetFullSqlForEntityType(
bool isContent,
bool isMedia,
bool isMember,
Guid[] objectTypes,
Ordering ordering,
Action<Sql<ISqlContext>>? filter)
{
Sql<ISqlContext> sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] { objectType });
Sql<ISqlContext> sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, objectTypes);
AddGroupBy(isContent, isMedia, isMember, sql, false);
ApplyOrdering(ref sql, ordering);
@@ -788,6 +815,8 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
Ordering? runner = ordering;
Direction lastDirection = Direction.Ascending;
bool orderingIncludesNodeId = false;
do
{
@@ -799,7 +828,10 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
case "PATH":
orderBy = SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path");
break;
case "NODEID":
orderBy = runner.OrderBy;
orderingIncludesNodeId = true;
break;
default:
orderBy = runner.OrderBy ?? string.Empty;
break;
@@ -814,11 +846,25 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
sql.OrderByDescending(orderBy);
}
lastDirection = runner.Direction;
runner = runner.Next;
}
while (runner is not null);
// If we haven't already included the node Id in the order by clause, order by node Id as well to ensure consistent results
// when the provided sort yields entities with the same value.
if (orderingIncludesNodeId is false)
{
if (lastDirection == Direction.Ascending)
{
sql.OrderBy<NodeDto>(x => x.NodeId);
}
else
{
sql.OrderByDescending<NodeDto>(x => x.NodeId);
}
}
}
#endregion

View File

@@ -385,9 +385,14 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
Assert.That(total, Is.EqualTo(10));
}
[Test]
public async Task EntityService_Can_Get_Paged_Document_Type_Children()
[TestCase("")]
[TestCase("sortOrder")]
[TestCase("nodeId")]
public async Task EntityService_Can_Get_Paged_Document_Type_Children(string orderBy)
{
Ordering? ordering = string.IsNullOrEmpty(orderBy)
? null
: Ordering.By(orderBy, Direction.Ascending);
IEnumerable<IEntitySlim> children = EntityService.GetPagedChildren(
_documentTypeRootContainerKey,
[UmbracoObjectTypes.DocumentTypeContainer],
@@ -395,13 +400,14 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
0,
10,
false,
out long totalRecords);
out long totalRecords,
ordering: ordering);
Assert.AreEqual(3, totalRecords);
Assert.AreEqual(3, children.Count());
Assert.IsTrue(children.Single(x => x.Key == _documentTypeSubContainer1Key).HasChildren); // Has a single folder as a child.
Assert.IsTrue(children.Single(x => x.Key == _documentTypeSubContainer2Key).HasChildren); // Has a single document type as a child.
Assert.IsFalse(children.Single(x => x.Key == _documentType1Key).HasChildren); // Is a document type (has no children).
Assert.IsFalse(children.Single(x => x.Key == _documentType1Key).HasChildren); // Is a document type (has no children).
}
[Test]
@@ -934,11 +940,11 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
[Test]
public void EntityService_Siblings_ReturnsExpectedSiblings()
{
var children = CreateSiblingsTestData();
var children = CreateDocumentSiblingsTestData();
var target = children[1];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 1, 1, out long totalBefore, out long totalAfter).ToArray();
var result = EntityService.GetSiblings(target.Key, [UmbracoObjectTypes.Document], 1, 1, out long totalBefore, out long totalAfter).ToArray();
Assert.AreEqual(0, totalBefore);
Assert.AreEqual(7, totalAfter);
Assert.AreEqual(3, result.Length);
@@ -950,13 +956,13 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
[Test]
public void EntityService_Siblings_SkipsTrashedEntities()
{
var children = CreateSiblingsTestData();
var children = CreateDocumentSiblingsTestData();
var trash = children[1];
ContentService.MoveToRecycleBin(trash);
var target = children[2];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 1, 1, out long totalBefore, out long totalAfter).ToArray();
var result = EntityService.GetSiblings(target.Key, [UmbracoObjectTypes.Document], 1, 1, out long totalBefore, out long totalAfter).ToArray();
Assert.AreEqual(0, totalBefore);
Assert.AreEqual(6, totalAfter);
Assert.AreEqual(3, result.Length);
@@ -969,7 +975,7 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
[Test]
public void EntityService_Siblings_SkipsFilteredEntities_UsingFilterWithSet()
{
var children = CreateSiblingsTestData();
var children = CreateDocumentSiblingsTestData();
// Apply a filter that excludes the child at index 1. We'd expect to not get this, but
// get still get one previous sibling, i.e. the entity at index 0.
@@ -977,7 +983,7 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
IQuery<IUmbracoEntity> filter = ScopeProvider.CreateQuery<IUmbracoEntity>().Where(x => !keysToExclude.Contains(x.Key));
var target = children[2];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 1, 1, out long totalBefore, out long totalAfter, filter).ToArray();
var result = EntityService.GetSiblings(target.Key, [UmbracoObjectTypes.Document], 1, 1, out long totalBefore, out long totalAfter, filter).ToArray();
Assert.AreEqual(0, totalBefore);
Assert.AreEqual(6, totalAfter);
Assert.AreEqual(3, result.Length);
@@ -990,7 +996,7 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
[Test]
public void EntityService_Siblings_SkipsFilteredEntities_UsingFilterWithoutSet()
{
var children = CreateSiblingsTestData();
var children = CreateDocumentSiblingsTestData();
// Apply a filter that excludes the child at index 1. We'd expect to not get this, but
// get still get one previous sibling, i.e. the entity at index 0.
@@ -998,7 +1004,7 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
IQuery<IUmbracoEntity> filter = ScopeProvider.CreateQuery<IUmbracoEntity>().Where(x => x.Key != keyToExclude);
var target = children[2];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 1, 1, out long totalBefore, out long totalAfter, filter).ToArray();
var result = EntityService.GetSiblings(target.Key, [UmbracoObjectTypes.Document], 1, 1, out long totalBefore, out long totalAfter, filter).ToArray();
Assert.AreEqual(0, totalBefore);
Assert.AreEqual(6, totalAfter);
Assert.AreEqual(3, result.Length);
@@ -1011,13 +1017,13 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
[Test]
public void EntityService_Siblings_RespectsOrdering()
{
var children = CreateSiblingsTestData();
var children = CreateDocumentSiblingsTestData();
// Order the children by name to ensure the ordering works when differing from the default sort order, the name is a GUID.
children = children.OrderBy(x => x.Name).ToList();
var target = children[1];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 1, 1, out long totalBefore, out long totalAfter, ordering: Ordering.By(nameof(NodeDto.Text))).ToArray();
var result = EntityService.GetSiblings(target.Key, [UmbracoObjectTypes.Document], 1, 1, out long totalBefore, out long totalAfter, ordering: Ordering.By(nameof(NodeDto.Text))).ToArray();
Assert.AreEqual(0, totalBefore);
Assert.AreEqual(7, totalAfter);
Assert.AreEqual(3, result.Length);
@@ -1029,10 +1035,10 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
[Test]
public void EntityService_Siblings_IgnoresOutOfBoundsLower()
{
var children = CreateSiblingsTestData();
var children = CreateDocumentSiblingsTestData();
var target = children[1];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 100, 1, out long totalBefore, out long totalAfter).ToArray();
var result = EntityService.GetSiblings(target.Key, [UmbracoObjectTypes.Document], 100, 1, out long totalBefore, out long totalAfter).ToArray();
Assert.AreEqual(0, totalBefore);
Assert.AreEqual(7, totalAfter);
Assert.AreEqual(3, result.Length);
@@ -1044,10 +1050,10 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
[Test]
public void EntityService_Siblings_IgnoresOutOfBoundsUpper()
{
var children = CreateSiblingsTestData();
var children = CreateDocumentSiblingsTestData();
var target = children[^2];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 1, 100, out long totalBefore, out long totalAfter).ToArray();
var result = EntityService.GetSiblings(target.Key, [UmbracoObjectTypes.Document], 1, 100, out long totalBefore, out long totalAfter).ToArray();
Assert.AreEqual(7, totalBefore);
Assert.AreEqual(0, totalAfter);
Assert.AreEqual(3, result.Length);
@@ -1056,7 +1062,33 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
Assert.IsTrue(result[^3].Key == children[^3].Key);
}
private List<Content> CreateSiblingsTestData()
[TestCase(true)]
[TestCase(false)]
public async Task EntityService_Siblings_FiltersByObjectTypes(bool foldersOnly)
{
// For testing these scenarios we can use the data setup in CreateStructureForPagedDocumentTypeChildrenTest.
var objectTypes = new List<UmbracoObjectTypes> { UmbracoObjectTypes.DocumentTypeContainer };
if (foldersOnly is false)
{
objectTypes.Add(UmbracoObjectTypes.DocumentType);
}
var result = EntityService.GetSiblings(_documentTypeSubContainer2Key, objectTypes, 1, 1, out long totalBefore, out long totalAfter).ToArray();
Assert.AreEqual(0, totalBefore);
Assert.AreEqual(0, totalAfter);
var expectedCount = foldersOnly ? 2 : 3;
Assert.AreEqual(expectedCount, result.Length);
Assert.IsTrue(result[0].Key == _documentTypeSubContainer1Key);
Assert.IsTrue(result[1].Key == _documentTypeSubContainer2Key);
if (foldersOnly is false)
{
Assert.IsTrue(result[2].Key == _documentType1Key);
}
}
private List<Content> CreateDocumentSiblingsTestData()
{
var contentType = ContentTypeService.Get("umbTextpage");

View File

@@ -11,7 +11,7 @@
},
"Tests": {
"Database": {
"DatabaseType": "SQLite",
"DatabaseType": "SQLite", // "SQLite", "LocalDb"
"PrepareThreadCount": 4,
"SchemaDatabaseCount": 4,
"EmptyDatabasesCount": 2,

View File

@@ -23,7 +23,7 @@ public class EntityServiceTests
if (shouldThrow)
{
Assert.Throws<ArgumentOutOfRangeException>(() => sut.GetSiblings(Guid.NewGuid(), UmbracoObjectTypes.Document, before, after, out _, out _));
Assert.Throws<ArgumentOutOfRangeException>(() => sut.GetSiblings(Guid.NewGuid(), [UmbracoObjectTypes.Document], before, after, out _, out _));
}
}