Skip to content

Commit 9f05ee1

Browse files
committed
Optimization: skip COUNT query when pagination is disabled
1 parent 4c0811e commit 9f05ee1

14 files changed

Lines changed: 216 additions & 635 deletions

File tree

src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,27 @@ public Task<IReadOnlyCollection<TResource>> GetAsync(CancellationToken cancellat
5050
{
5151
LogFiltersInTopScope();
5252

53-
if (SetPrimaryTotalCountIsZero())
53+
QueryLayer queryLayer = _queryLayerComposer.ComposeFromConstraints(_resourceType);
54+
int? pageSize = queryLayer.Pagination?.PageSize?.Value;
55+
56+
if (_options.IncludeTotalResourceCount && pageSize != null)
5457
{
55-
return Task.FromResult<IReadOnlyCollection<TResource>>(Array.Empty<TResource>());
56-
}
58+
_paginationContext.TotalResourceCount = GetResourceCountForPrimaryEndpoint(queryLayer.Filter);
5759

58-
QueryLayer queryLayer = _queryLayerComposer.ComposeFromConstraints(_resourceType);
60+
if (_paginationContext.TotalResourceCount == 0)
61+
{
62+
return Task.FromResult<IReadOnlyCollection<TResource>>(Array.Empty<TResource>());
63+
}
64+
}
5965

6066
IEnumerable<TResource> dataSource = GetDataSource(_resourceType).Cast<TResource>();
6167
TResource[] resources = _queryLayerToLinqConverter.ApplyQueryLayer(queryLayer, dataSource).ToArray();
6268

63-
if (queryLayer.Pagination?.PageSize?.Value == resources.Length)
69+
if (pageSize == null)
70+
{
71+
_paginationContext.TotalResourceCount = resources.Length;
72+
}
73+
else if (pageSize == resources.Length)
6474
{
6575
_paginationContext.IsPageFull = true;
6676
}
@@ -91,27 +101,17 @@ private void LogFiltersInTopScope()
91101
}
92102
}
93103

94-
private bool SetPrimaryTotalCountIsZero()
104+
private int GetResourceCountForPrimaryEndpoint(FilterExpression? filter)
95105
{
96-
if (_options.IncludeTotalResourceCount)
106+
var queryLayer = new QueryLayer(_resourceType)
97107
{
98-
var queryLayer = new QueryLayer(_resourceType)
99-
{
100-
Filter = _queryLayerComposer.GetPrimaryFilterFromConstraints(_resourceType)
101-
};
108+
Filter = filter
109+
};
102110

103-
IEnumerable<TResource> dataSource = GetDataSource(_resourceType).Cast<TResource>();
104-
IEnumerable<TResource> resources = _queryLayerToLinqConverter.ApplyQueryLayer(queryLayer, dataSource);
105-
106-
_paginationContext.TotalResourceCount = resources.Count();
107-
108-
if (_paginationContext.TotalResourceCount == 0)
109-
{
110-
return true;
111-
}
112-
}
111+
IEnumerable<TResource> dataSource = GetDataSource(_resourceType).Cast<TResource>();
112+
IEnumerable<TResource> resources = _queryLayerToLinqConverter.ApplyQueryLayer(queryLayer, dataSource);
113113

114-
return false;
114+
return resources.Count();
115115
}
116116

117117
/// <inheritdoc />
@@ -141,10 +141,14 @@ public Task<TResource> GetAsync([DisallowNull] TId id, CancellationToken cancell
141141
throw new RelationshipNotFoundException(relationshipName, _resourceType.PublicName);
142142
}
143143

144-
SetNonPrimaryTotalCount(id, relationship);
145-
146144
QueryLayer secondaryLayer = _queryLayerComposer.ComposeFromConstraints(relationship.RightType);
147145
QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _resourceType, id, relationship);
146+
int? pageSize = secondaryLayer.Pagination?.PageSize?.Value;
147+
148+
if (_options.IncludeTotalResourceCount && relationship is HasManyAttribute hasManyRelationship && pageSize != null)
149+
{
150+
SetResourceCountForNonPrimaryEndpoint(id, hasManyRelationship);
151+
}
148152

149153
IEnumerable<TResource> dataSource = GetDataSource(_resourceType).Cast<TResource>();
150154
IEnumerable<TResource> primaryResources = _queryLayerToLinqConverter.ApplyQueryLayer(primaryLayer, dataSource);
@@ -157,31 +161,35 @@ public Task<TResource> GetAsync([DisallowNull] TId id, CancellationToken cancell
157161

158162
object? rightValue = relationship.GetValue(primaryResource);
159163

160-
if (rightValue is ICollection rightResources && secondaryLayer.Pagination?.PageSize?.Value == rightResources.Count)
164+
if (rightValue is IEnumerable rightResources)
161165
{
162-
_paginationContext.IsPageFull = true;
166+
int resourceCount = rightResources.Cast<object>().Count();
167+
168+
if (pageSize == null)
169+
{
170+
_paginationContext.TotalResourceCount = resourceCount;
171+
}
172+
else if (pageSize == resourceCount)
173+
{
174+
_paginationContext.IsPageFull = true;
175+
}
163176
}
164177

165178
return Task.FromResult(rightValue);
166179
}
167180

168-
private void SetNonPrimaryTotalCount([DisallowNull] TId id, RelationshipAttribute relationship)
181+
private void SetResourceCountForNonPrimaryEndpoint([DisallowNull] TId id, HasManyAttribute relationship)
169182
{
170-
if (_options.IncludeTotalResourceCount && relationship is HasManyAttribute hasManyRelationship)
171-
{
172-
FilterExpression? secondaryFilter = _queryLayerComposer.GetSecondaryFilterFromConstraints(id, hasManyRelationship);
183+
FilterExpression? secondaryFilter = _queryLayerComposer.GetSecondaryFilterFromConstraints(id, relationship);
173184

174-
if (secondaryFilter == null)
175-
{
176-
return;
177-
}
178-
179-
var queryLayer = new QueryLayer(hasManyRelationship.RightType)
185+
if (secondaryFilter != null)
186+
{
187+
var queryLayer = new QueryLayer(relationship.RightType)
180188
{
181189
Filter = secondaryFilter
182190
};
183191

184-
IEnumerable<IIdentifiable> dataSource = GetDataSource(hasManyRelationship.RightType);
192+
IEnumerable<IIdentifiable> dataSource = GetDataSource(relationship.RightType);
185193
IEnumerable<IIdentifiable> resources = _queryLayerToLinqConverter.ApplyQueryLayer(queryLayer, dataSource);
186194

187195
_paginationContext.TotalResourceCount = resources.Count();

src/JsonApiDotNetCore.Annotations/CollectionConverter.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ public IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType
6060
/// </summary>
6161
private Type ToConcreteCollectionType(Type collectionType)
6262
{
63+
ArgumentNullException.ThrowIfNull(collectionType);
64+
6365
if (collectionType is { IsInterface: true, IsGenericType: true })
6466
{
6567
Type openCollectionType = collectionType.GetGenericTypeDefinition();
@@ -94,6 +96,23 @@ public IReadOnlyCollection<IIdentifiable> ExtractResources(object? value)
9496
};
9597
}
9698

99+
/// <summary>
100+
/// Returns the number of elements in a collection of resources.
101+
/// </summary>
102+
public int GetCount(IEnumerable source)
103+
{
104+
ArgumentNullException.ThrowIfNull(source);
105+
106+
return source switch
107+
{
108+
List<IIdentifiable> resourceList => resourceList.Count,
109+
HashSet<IIdentifiable> resourceSet => resourceSet.Count,
110+
IReadOnlyCollection<IIdentifiable> resourceCollection => resourceCollection.Count,
111+
IEnumerable<IIdentifiable> resources => resources.Count(),
112+
_ => source.Cast<object>().Count()
113+
};
114+
}
115+
97116
/// <summary>
98117
/// Returns the element type if the specified type is a generic collection, for example: <code><![CDATA[
99118
/// IList<string> -> string

src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public interface IQueryLayerComposer
1414
/// <summary>
1515
/// Builds a filter from constraints, used to determine total resource count on a primary collection endpoint.
1616
/// </summary>
17+
[Obsolete("This method is no longer used and will be removed in a future version.")]
18+
// ReSharper disable once UnusedMemberInSuper.Global
1719
FilterExpression? GetPrimaryFilterFromConstraints(ResourceType primaryResourceType);
1820

1921
/// <summary>

src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public QueryLayerComposer(IEnumerable<IQueryConstraintProvider> constraintProvid
4444
}
4545

4646
/// <inheritdoc />
47+
[Obsolete("This method is no longer used and will be removed in a future version.")]
4748
public FilterExpression? GetPrimaryFilterFromConstraints(ResourceType primaryResourceType)
4849
{
4950
// @formatter:wrap_chained_method_calls chop_always

src/JsonApiDotNetCore/Services/JsonApiResourceService.cs

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,26 @@ public virtual async Task<IReadOnlyCollection<TResource>> GetAsync(CancellationT
6262

6363
using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get resources");
6464

65-
if (_options.IncludeTotalResourceCount)
65+
QueryLayer queryLayer = _queryLayerComposer.ComposeFromConstraints(_request.PrimaryResourceType);
66+
int? pageSize = queryLayer.Pagination?.PageSize?.Value;
67+
68+
if (_options.IncludeTotalResourceCount && pageSize != null)
6669
{
67-
FilterExpression? topFilter = _queryLayerComposer.GetPrimaryFilterFromConstraints(_request.PrimaryResourceType);
68-
_paginationContext.TotalResourceCount = await _repositoryAccessor.CountAsync(_request.PrimaryResourceType, topFilter, cancellationToken);
70+
_paginationContext.TotalResourceCount = await _repositoryAccessor.CountAsync(_request.PrimaryResourceType, queryLayer.Filter, cancellationToken);
6971

7072
if (_paginationContext.TotalResourceCount == 0)
7173
{
7274
return Array.Empty<TResource>();
7375
}
7476
}
7577

76-
QueryLayer queryLayer = _queryLayerComposer.ComposeFromConstraints(_request.PrimaryResourceType);
7778
IReadOnlyCollection<TResource> resources = await _repositoryAccessor.GetAsync<TResource>(queryLayer, cancellationToken);
7879

79-
if (queryLayer.Pagination?.PageSize?.Value == resources.Count)
80+
if (pageSize == null)
81+
{
82+
_paginationContext.TotalResourceCount = resources.Count;
83+
}
84+
else if (pageSize == resources.Count)
8085
{
8186
_paginationContext.IsPageFull = true;
8287
}
@@ -107,31 +112,43 @@ public virtual async Task<TResource> GetAsync([DisallowNull] TId id, Cancellatio
107112
});
108113

109114
ArgumentNullException.ThrowIfNull(relationshipName);
110-
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);
111115
AssertHasRelationship(_request.Relationship, relationshipName);
116+
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);
117+
AssertSecondaryResourceTypeInJsonApiRequestIsNotNull(_request.SecondaryResourceType);
112118

113119
using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get secondary resource(s)");
114120

115-
if (_options.IncludeTotalResourceCount && _request.IsCollection)
121+
QueryLayer secondaryLayer = _queryLayerComposer.ComposeFromConstraints(_request.SecondaryResourceType);
122+
QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResourceType, id, _request.Relationship);
123+
int? pageSize = secondaryLayer.Pagination?.PageSize?.Value;
124+
125+
if (_options.IncludeTotalResourceCount && _request.IsCollection && pageSize != null)
116126
{
117127
await RetrieveResourceCountForNonPrimaryEndpointAsync(id, (HasManyAttribute)_request.Relationship, cancellationToken);
118128

119129
// We cannot return early when _paginationContext.TotalResourceCount == 0, because we don't know whether
120130
// the parent resource exists. In case the parent does not exist, an error is produced below.
121131
}
122132

123-
QueryLayer secondaryLayer = _queryLayerComposer.ComposeFromConstraints(_request.SecondaryResourceType!);
124-
QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResourceType, id, _request.Relationship);
125133
IReadOnlyCollection<TResource> primaryResources = await _repositoryAccessor.GetAsync<TResource>(primaryLayer, cancellationToken);
126134

127135
TResource? primaryResource = primaryResources.SingleOrDefault();
128136
AssertPrimaryResourceExists(primaryResource);
129137

130138
object? rightValue = _request.Relationship.GetValue(primaryResource);
131139

132-
if (rightValue is ICollection rightResources && secondaryLayer.Pagination?.PageSize?.Value == rightResources.Count)
140+
if (rightValue is IEnumerable rightResources)
133141
{
134-
_paginationContext.IsPageFull = true;
142+
int resourceCount = CollectionConverter.Instance.GetCount(rightResources);
143+
144+
if (pageSize == null)
145+
{
146+
_paginationContext.TotalResourceCount = resourceCount;
147+
}
148+
else if (pageSize == resourceCount)
149+
{
150+
_paginationContext.IsPageFull = true;
151+
}
135152
}
136153

137154
return rightValue;
@@ -147,31 +164,43 @@ public virtual async Task<TResource> GetAsync([DisallowNull] TId id, Cancellatio
147164
});
148165

149166
ArgumentNullException.ThrowIfNull(relationshipName);
150-
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);
151167
AssertHasRelationship(_request.Relationship, relationshipName);
168+
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);
169+
AssertSecondaryResourceTypeInJsonApiRequestIsNotNull(_request.SecondaryResourceType);
152170

153171
using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get relationship");
154172

155-
if (_options.IncludeTotalResourceCount && _request.IsCollection)
173+
QueryLayer secondaryLayer = _queryLayerComposer.ComposeSecondaryLayerForRelationship(_request.SecondaryResourceType);
174+
QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResourceType, id, _request.Relationship);
175+
int? pageSize = secondaryLayer.Pagination?.PageSize?.Value;
176+
177+
if (_options.IncludeTotalResourceCount && _request.IsCollection && pageSize != null)
156178
{
157179
await RetrieveResourceCountForNonPrimaryEndpointAsync(id, (HasManyAttribute)_request.Relationship, cancellationToken);
158180

159181
// We cannot return early when _paginationContext.TotalResourceCount == 0, because we don't know whether
160182
// the parent resource exists. In case the parent does not exist, an error is produced below.
161183
}
162184

163-
QueryLayer secondaryLayer = _queryLayerComposer.ComposeSecondaryLayerForRelationship(_request.SecondaryResourceType!);
164-
QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResourceType, id, _request.Relationship);
165185
IReadOnlyCollection<TResource> primaryResources = await _repositoryAccessor.GetAsync<TResource>(primaryLayer, cancellationToken);
166186

167187
TResource? primaryResource = primaryResources.SingleOrDefault();
168188
AssertPrimaryResourceExists(primaryResource);
169189

170190
object? rightValue = _request.Relationship.GetValue(primaryResource);
171191

172-
if (rightValue is ICollection rightResources && secondaryLayer.Pagination?.PageSize?.Value == rightResources.Count)
192+
if (rightValue is IEnumerable rightResources)
173193
{
174-
_paginationContext.IsPageFull = true;
194+
int resourceCount = CollectionConverter.Instance.GetCount(rightResources);
195+
196+
if (pageSize == null)
197+
{
198+
_paginationContext.TotalResourceCount = resourceCount;
199+
}
200+
else if (pageSize == resourceCount)
201+
{
202+
_paginationContext.IsPageFull = true;
203+
}
175204
}
176205

177206
return rightValue;
@@ -677,6 +706,16 @@ private void AssertPrimaryResourceTypeInJsonApiRequestIsNotNull([SysNotNull] Res
677706
}
678707
}
679708

709+
[AssertionMethod]
710+
private void AssertSecondaryResourceTypeInJsonApiRequestIsNotNull([SysNotNull] ResourceType? resourceType)
711+
{
712+
if (resourceType == null)
713+
{
714+
throw new InvalidOperationException(
715+
$"Expected {nameof(IJsonApiRequest)}.{nameof(IJsonApiRequest.SecondaryResourceType)} not to be null at this point.");
716+
}
717+
}
718+
680719
[AssertionMethod]
681720
private void AssertRelationshipInJsonApiRequestIsNotNull([SysNotNull] RelationshipAttribute? relationship)
682721
{

test/DapperTests/IntegrationTests/DapperTestContext.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,7 @@ private WebApplicationFactory<TodoItem> CreateFactory()
8181
builder.ConfigureServices(services =>
8282
{
8383
services.Replace(ServiceDescriptor.Singleton<TimeProvider>(new FrozenTimeProvider(FrozenTime)));
84-
85-
ServiceDescriptor scopedCaptureStore = services.Single(descriptor => descriptor.ImplementationType == typeof(SqlCaptureStore));
86-
services.Remove(scopedCaptureStore);
87-
88-
services.AddSingleton<SqlCaptureStore>();
84+
services.Replace(ServiceDescriptor.Singleton<SqlCaptureStore, SqlCaptureStore>());
8985
});
9086
});
9187
}

0 commit comments

Comments
 (0)