Skip to content

Commit cd8ae40

Browse files
authored
Enable to force-disable pagination per relationship (#1750)
* Add assertion method to clarify intent * Remove redundant nullability suppressions * Enable to enforce disabling pagination per relationship
1 parent f4980c3 commit cd8ae40

File tree

27 files changed

+351
-30
lines changed

27 files changed

+351
-30
lines changed

docs/build-dev.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ function EnsureHttpServerIsInstalled {
1818
throw "Unable to find npm in your PATH. please install Node.js first."
1919
}
2020

21+
# If this command fails with ENOENT after installing Node.js on Windows, manually create the directory %APPDATA%\npm.
2122
npm list --depth 1 --global httpserver >$null
2223

2324
if ($LastExitCode -eq 1) {

docs/usage/options.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ options.IncludeTotalResourceCount = true;
5959
To retrieve the total number of resources on secondary and relationship endpoints, the reverse of the relationship must to be available. For example, in `GET /customers/1/orders`, both the relationships `[HasMany] Customer.Orders` and `[HasOne] Order.Customer` must be defined.
6060
If `IncludeTotalResourceCount` is set to `false` (or the inverse relationship is unavailable on a non-primary endpoint), best-effort pagination links are returned instead. This means no `last` link and the `next` link only occurs when the current page is full.
6161

62+
> [!TIP]
63+
> Since v5.8, pagination can be [turned off per relationship](~/usage/resources/relationships.md#disable-pagination).
64+
6265
## Relative Links
6366

6467
All links are absolute by default. However, you can configure relative links:

docs/usage/reading/pagination.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ GET /api/blogs/1/articles?include=revisions&page[size]=10,revisions:5&page[numbe
2525
## Configuring Default Behavior
2626

2727
You can configure the global default behavior as described [here](~/usage/options.md#pagination).
28+
29+
> [!TIP]
30+
> Since v5.8, pagination can be [turned off per relationship](~/usage/resources/relationships.md#disable-pagination).

docs/usage/resources/relationships.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,17 @@ public class Person : Identifiable<int>
213213

214214
The left side of this relationship is of type `Person` (public name: "persons") and the right side is of type `TodoItem` (public name: "todoItems").
215215

216+
### Disable pagination
217+
218+
_since v5.8_
219+
220+
Pagination can be turned off per to-many relationship by setting `DisablePagination` to `true`.
221+
When doing so, it overrules the global pagination settings in options, and any pagination used in the query string
222+
for the relationship.
223+
224+
This feature exists for cases where the number of *related* resources is typically small.
225+
For example, while the number of products is usually high, the number of products *in a shopping basket* is not.
226+
216227
## HasManyThrough
217228

218229
_removed since v5.0_

src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasManyAttribute.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ public HasManyCapabilities Capabilities
5050
set => _capabilities = value;
5151
}
5252

53+
/// <summary>
54+
/// When set to <c>true</c>, overrules the default page size, the page size from a resource definition, and the
55+
/// <c>
56+
/// page[size]
57+
/// </c>
58+
/// query string parameter by forcibly turning off pagination on the related resources for this relationship.
59+
/// </summary>
60+
/// <remarks>
61+
/// Caution: only use this when the number of related resources (along with their nested includes) is known to always be small.
62+
/// </remarks>
63+
public bool DisablePagination { get; set; }
64+
5365
public HasManyAttribute()
5466
{
5567
_lazyIsManyToMany = new Lazy<bool>(EvaluateIsManyToMany, LazyThreadSafetyMode.PublicationOnly);

src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasManyAttribute.netstandard.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@ public sealed class HasManyAttribute : RelationshipAttribute
1111
{
1212
/// <summary />
1313
public HasManyCapabilities Capabilities { get; set; }
14+
15+
/// <summary />
16+
public bool DisablePagination { get; set; }
1417
}

src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ public interface IJsonApiOptions
105105
bool IncludeTotalResourceCount { get; }
106106

107107
/// <summary>
108-
/// The page size (10 by default) that is used when not specified in query string. Set to <c>null</c> to not use pagination by default.
108+
/// The page size (10 by default) that is used when not specified in query string. Set to <c>null</c> to not use pagination by default. This setting can
109+
/// be overruled per relationship by setting <see cref="HasManyAttribute.DisablePagination" /> to <c>true</c>.
109110
/// </summary>
110111
PageSize? DefaultPageSize { get; }
111112

src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public override QueryExpression VisitIsType(IsTypeExpression expression, TArgume
124124

125125
if (newElements.Count != 0)
126126
{
127-
var newExpression = new SortExpression(newElements);
127+
var newExpression = new SortExpression(newElements, expression.IsAutoGenerated);
128128
return newExpression.Equals(expression) ? expression : newExpression;
129129
}
130130

src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,27 @@ namespace JsonApiDotNetCore.Queries.Expressions;
1313
[PublicAPI]
1414
public class SortExpression : QueryExpression
1515
{
16+
/// <summary>
17+
/// Indicates whether this expression was generated by JsonApiDotNetCore to ensure a deterministic order.
18+
/// </summary>
19+
internal bool IsAutoGenerated { get; }
20+
1621
/// <summary>
1722
/// One or more elements to sort on.
1823
/// </summary>
1924
public IImmutableList<SortElementExpression> Elements { get; }
2025

2126
public SortExpression(IImmutableList<SortElementExpression> elements)
27+
: this(elements, false)
28+
{
29+
}
30+
31+
internal SortExpression(IImmutableList<SortElementExpression> elements, bool isAutoGenerated)
2232
{
2333
ArgumentGuard.NotNullNorEmpty(elements);
2434

2535
Elements = elements;
36+
IsAutoGenerated = isAutoGenerated;
2637
}
2738

2839
public override TResult Accept<TArgument, TResult>(QueryExpressionVisitor<TArgument, TResult> visitor, TArgument argument)
@@ -37,7 +48,7 @@ public override string ToString()
3748

3849
public override string ToFullString()
3950
{
40-
return string.Join(",", Elements.Select(child => child.ToFullString()));
51+
return $"{string.Join(",", Elements.Select(child => child.ToFullString()))}{(IsAutoGenerated ? " (auto-generated)" : "")}";
4152
}
4253

4354
public override bool Equals(object? obj)
@@ -54,12 +65,13 @@ public override bool Equals(object? obj)
5465

5566
var other = (SortExpression)obj;
5667

57-
return Elements.SequenceEqual(other.Elements);
68+
return IsAutoGenerated == other.IsAutoGenerated && Elements.SequenceEqual(other.Elements);
5869
}
5970

6071
public override int GetHashCode()
6172
{
6273
var hashCode = new HashCode();
74+
hashCode.Add(IsAutoGenerated);
6375

6476
foreach (SortElementExpression element in Elements)
6577
{

src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,12 +243,13 @@ private IImmutableSet<IncludeElementExpression> ProcessIncludeSet(IImmutableSet<
243243

244244
ResourceType resourceType = includeElement.Relationship.RightType;
245245
bool isToManyRelationship = includeElement.Relationship is HasManyAttribute;
246+
bool allowPagination = includeElement.Relationship is HasManyAttribute { DisablePagination: false };
246247

247248
var subLayer = new QueryLayer(resourceType)
248249
{
249250
Filter = isToManyRelationship ? GetFilter(expressionsInCurrentScope, resourceType) : null,
250251
Sort = isToManyRelationship ? GetSort(expressionsInCurrentScope, resourceType) : null,
251-
Pagination = isToManyRelationship ? GetPagination(expressionsInCurrentScope, resourceType) : null,
252+
Pagination = allowPagination ? GetPagination(expressionsInCurrentScope, resourceType) : null,
252253
Selection = GetSelectionForSparseAttributeSet(resourceType)
253254
};
254255

@@ -384,12 +385,26 @@ public QueryLayer WrapLayerForSecondaryEndpoint<TId>(QueryLayer secondaryLayer,
384385
FilterExpression? primaryFilter = GetFilter(Array.Empty<QueryExpression>(), primaryResourceType);
385386
AttrAttribute primaryIdAttribute = GetIdAttribute(primaryResourceType);
386387

387-
return new QueryLayer(primaryResourceType)
388+
var primaryLayer = new QueryLayer(primaryResourceType)
388389
{
389390
Include = RewriteIncludeForSecondaryEndpoint(innerInclude, relationship),
390391
Filter = CreateFilterByIds([primaryId], primaryIdAttribute, primaryFilter),
391392
Selection = primarySelection
392393
};
394+
395+
if (relationship is HasManyAttribute { DisablePagination: true } && secondaryLayer.Pagination != null)
396+
{
397+
// Undo pagination/sort. At the time secondaryLayer was being built, we were not yet aware that it needed to be turned off.
398+
secondaryLayer.Pagination = null;
399+
_paginationContext.PageSize = null;
400+
401+
if (secondaryLayer.Sort is { IsAutoGenerated: true })
402+
{
403+
secondaryLayer.Sort = null;
404+
}
405+
}
406+
407+
return primaryLayer;
393408
}
394409

395410
private IncludeExpression RewriteIncludeForSecondaryEndpoint(IncludeExpression? relativeInclude, RelationshipAttribute secondaryRelationship)
@@ -554,7 +569,7 @@ private SortExpression CreateSortById(ResourceType resourceType)
554569
{
555570
AttrAttribute idAttribute = GetIdAttribute(resourceType);
556571
var idAscendingSort = new SortElementExpression(new ResourceFieldChainExpression(idAttribute), true);
557-
return new SortExpression(ImmutableArray.Create(idAscendingSort));
572+
return new SortExpression(ImmutableArray.Create(idAscendingSort), true);
558573
}
559574

560575
protected virtual PaginationExpression GetPagination(IReadOnlyCollection<QueryExpression> expressionsInScope, ResourceType resourceType)

0 commit comments

Comments
 (0)