Skip to content

Commit 196566e

Browse files
committed
Enable to enforce pagination disabled per relationship
1 parent db8ee0a commit 196566e

File tree

11 files changed

+300
-9
lines changed

11 files changed

+300
-9
lines changed

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: 17 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 = isToManyRelationship && allowPagination ? GetPagination(expressionsInCurrentScope, resourceType) : null,
252253
Selection = GetSelectionForSparseAttributeSet(resourceType)
253254
};
254255

@@ -384,12 +385,25 @@ 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+
secondaryLayer.Pagination = null;
398+
_paginationContext.PageSize = null;
399+
400+
if (secondaryLayer.Sort is { IsAutoGenerated: true })
401+
{
402+
secondaryLayer.Sort = null;
403+
}
404+
}
405+
406+
return primaryLayer;
393407
}
394408

395409
private IncludeExpression RewriteIncludeForSecondaryEndpoint(IncludeExpression? relativeInclude, RelationshipAttribute secondaryRelationship)
@@ -554,7 +568,7 @@ private SortExpression CreateSortById(ResourceType resourceType)
554568
{
555569
AttrAttribute idAttribute = GetIdAttribute(resourceType);
556570
var idAscendingSort = new SortElementExpression(new ResourceFieldChainExpression(idAttribute), true);
557-
return new SortExpression(ImmutableArray.Create(idAscendingSort));
571+
return new SortExpression(ImmutableArray.Create(idAscendingSort), true);
558572
}
559573

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

test/AnnotationTests/Models/TreeNode.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public sealed class TreeNode : Identifiable<long>
1717
[HasOne(PublicName = "orders", Capabilities = HasOneCapabilities.AllowView | HasOneCapabilities.AllowInclude, Links = LinkTypes.All)]
1818
public TreeNode? Parent { get; set; }
1919

20-
[HasMany(PublicName = "orders", Capabilities = HasManyCapabilities.AllowView | HasManyCapabilities.AllowFilter, Links = LinkTypes.All)]
20+
[HasMany(PublicName = "orders", Capabilities = HasManyCapabilities.AllowView | HasManyCapabilities.AllowFilter, Links = LinkTypes.All,
21+
DisablePagination = true)]
2122
public ISet<TreeNode> Children { get; set; } = new HashSet<TreeNode>();
2223
}

test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Appointment.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public sealed class Appointment : Identifiable<int>
2020
[Attr]
2121
public DateTimeOffset EndTime { get; set; }
2222

23-
[HasMany]
23+
[HasOne]
24+
public Calendar? Calendar { get; set; }
25+
26+
[HasMany(DisablePagination = true)]
2427
public IList<Reminder> Reminders { get; set; } = new List<Reminder>();
2528
}

0 commit comments

Comments
 (0)