Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,27 @@ public Task<IReadOnlyCollection<TResource>> GetAsync(CancellationToken cancellat
{
LogFiltersInTopScope();

if (SetPrimaryTotalCountIsZero())
QueryLayer queryLayer = _queryLayerComposer.ComposeFromConstraints(_resourceType);
int? pageSize = queryLayer.Pagination?.PageSize?.Value;

if (_options.IncludeTotalResourceCount && pageSize != null)
{
return Task.FromResult<IReadOnlyCollection<TResource>>(Array.Empty<TResource>());
}
_paginationContext.TotalResourceCount = GetResourceCountForPrimaryEndpoint(queryLayer.Filter);

QueryLayer queryLayer = _queryLayerComposer.ComposeFromConstraints(_resourceType);
if (_paginationContext.TotalResourceCount == 0)
{
return Task.FromResult<IReadOnlyCollection<TResource>>(Array.Empty<TResource>());
}
}

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

if (queryLayer.Pagination?.PageSize?.Value == resources.Length)
if (pageSize == null)
{
_paginationContext.TotalResourceCount = resources.Length;
}
else if (pageSize == resources.Length)
{
_paginationContext.IsPageFull = true;
}
Expand Down Expand Up @@ -91,27 +101,17 @@ private void LogFiltersInTopScope()
}
}

private bool SetPrimaryTotalCountIsZero()
private int GetResourceCountForPrimaryEndpoint(FilterExpression? filter)
{
if (_options.IncludeTotalResourceCount)
var queryLayer = new QueryLayer(_resourceType)
{
var queryLayer = new QueryLayer(_resourceType)
{
Filter = _queryLayerComposer.GetPrimaryFilterFromConstraints(_resourceType)
};
Filter = filter
};

IEnumerable<TResource> dataSource = GetDataSource(_resourceType).Cast<TResource>();
IEnumerable<TResource> resources = _queryLayerToLinqConverter.ApplyQueryLayer(queryLayer, dataSource);

_paginationContext.TotalResourceCount = resources.Count();

if (_paginationContext.TotalResourceCount == 0)
{
return true;
}
}
IEnumerable<TResource> dataSource = GetDataSource(_resourceType).Cast<TResource>();
IEnumerable<TResource> resources = _queryLayerToLinqConverter.ApplyQueryLayer(queryLayer, dataSource);

return false;
return resources.Count();
}

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

SetNonPrimaryTotalCount(id, relationship);

QueryLayer secondaryLayer = _queryLayerComposer.ComposeFromConstraints(relationship.RightType);
QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _resourceType, id, relationship);
int? pageSize = secondaryLayer.Pagination?.PageSize?.Value;

if (_options.IncludeTotalResourceCount && relationship is HasManyAttribute hasManyRelationship && pageSize != null)
{
SetResourceCountForNonPrimaryEndpoint(id, hasManyRelationship);
}

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

object? rightValue = relationship.GetValue(primaryResource);

if (rightValue is ICollection rightResources && secondaryLayer.Pagination?.PageSize?.Value == rightResources.Count)
if (rightValue is IEnumerable rightResources)
{
_paginationContext.IsPageFull = true;
int resourceCount = rightResources.Cast<object>().Count();

if (pageSize == null)
{
_paginationContext.TotalResourceCount = resourceCount;
}
else if (pageSize == resourceCount)
{
_paginationContext.IsPageFull = true;
}
}

return Task.FromResult(rightValue);
}

private void SetNonPrimaryTotalCount([DisallowNull] TId id, RelationshipAttribute relationship)
private void SetResourceCountForNonPrimaryEndpoint([DisallowNull] TId id, HasManyAttribute relationship)
{
if (_options.IncludeTotalResourceCount && relationship is HasManyAttribute hasManyRelationship)
{
FilterExpression? secondaryFilter = _queryLayerComposer.GetSecondaryFilterFromConstraints(id, hasManyRelationship);
FilterExpression? secondaryFilter = _queryLayerComposer.GetSecondaryFilterFromConstraints(id, relationship);

if (secondaryFilter == null)
{
return;
}

var queryLayer = new QueryLayer(hasManyRelationship.RightType)
if (secondaryFilter != null)
{
var queryLayer = new QueryLayer(relationship.RightType)
{
Filter = secondaryFilter
};

IEnumerable<IIdentifiable> dataSource = GetDataSource(hasManyRelationship.RightType);
IEnumerable<IIdentifiable> dataSource = GetDataSource(relationship.RightType);
IEnumerable<IIdentifiable> resources = _queryLayerToLinqConverter.ApplyQueryLayer(queryLayer, dataSource);

_paginationContext.TotalResourceCount = resources.Count();
Expand Down
19 changes: 19 additions & 0 deletions src/JsonApiDotNetCore.Annotations/CollectionConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ public IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType
/// </summary>
private Type ToConcreteCollectionType(Type collectionType)
{
ArgumentNullException.ThrowIfNull(collectionType);

if (collectionType is { IsInterface: true, IsGenericType: true })
{
Type openCollectionType = collectionType.GetGenericTypeDefinition();
Expand Down Expand Up @@ -94,6 +96,23 @@ public IReadOnlyCollection<IIdentifiable> ExtractResources(object? value)
};
}

/// <summary>
/// Returns the number of elements in a collection of resources.
/// </summary>
public int GetCount(IEnumerable source)
{
ArgumentNullException.ThrowIfNull(source);

return source switch
{
List<IIdentifiable> resourceList => resourceList.Count,
HashSet<IIdentifiable> resourceSet => resourceSet.Count,
IReadOnlyCollection<IIdentifiable> resourceCollection => resourceCollection.Count,
IEnumerable<IIdentifiable> resources => resources.Count(),
_ => source.Cast<object>().Count()
};
}

/// <summary>
/// Returns the element type if the specified type is a generic collection, for example: <code><![CDATA[
/// IList<string> -> string
Expand Down
2 changes: 2 additions & 0 deletions src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public interface IQueryLayerComposer
/// <summary>
/// Builds a filter from constraints, used to determine total resource count on a primary collection endpoint.
/// </summary>
[Obsolete("This method is no longer used and will be removed in a future version.")]
// ReSharper disable once UnusedMemberInSuper.Global
FilterExpression? GetPrimaryFilterFromConstraints(ResourceType primaryResourceType);

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public QueryLayerComposer(IEnumerable<IQueryConstraintProvider> constraintProvid
}

/// <inheritdoc />
[Obsolete("This method is no longer used and will be removed in a future version.")]
public FilterExpression? GetPrimaryFilterFromConstraints(ResourceType primaryResourceType)
{
// @formatter:wrap_chained_method_calls chop_always
Expand Down
73 changes: 56 additions & 17 deletions src/JsonApiDotNetCore/Services/JsonApiResourceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,26 @@ public virtual async Task<IReadOnlyCollection<TResource>> GetAsync(CancellationT

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

if (_options.IncludeTotalResourceCount)
QueryLayer queryLayer = _queryLayerComposer.ComposeFromConstraints(_request.PrimaryResourceType);
int? pageSize = queryLayer.Pagination?.PageSize?.Value;

if (_options.IncludeTotalResourceCount && pageSize != null)
{
FilterExpression? topFilter = _queryLayerComposer.GetPrimaryFilterFromConstraints(_request.PrimaryResourceType);
_paginationContext.TotalResourceCount = await _repositoryAccessor.CountAsync(_request.PrimaryResourceType, topFilter, cancellationToken);
_paginationContext.TotalResourceCount = await _repositoryAccessor.CountAsync(_request.PrimaryResourceType, queryLayer.Filter, cancellationToken);

if (_paginationContext.TotalResourceCount == 0)
{
return Array.Empty<TResource>();
}
}

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

if (queryLayer.Pagination?.PageSize?.Value == resources.Count)
if (pageSize == null)
{
_paginationContext.TotalResourceCount = resources.Count;
}
else if (pageSize == resources.Count)
{
_paginationContext.IsPageFull = true;
}
Expand Down Expand Up @@ -107,31 +112,43 @@ public virtual async Task<TResource> GetAsync([DisallowNull] TId id, Cancellatio
});

ArgumentNullException.ThrowIfNull(relationshipName);
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);
AssertHasRelationship(_request.Relationship, relationshipName);
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);
AssertSecondaryResourceTypeInJsonApiRequestIsNotNull(_request.SecondaryResourceType);

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

if (_options.IncludeTotalResourceCount && _request.IsCollection)
QueryLayer secondaryLayer = _queryLayerComposer.ComposeFromConstraints(_request.SecondaryResourceType);
QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResourceType, id, _request.Relationship);
int? pageSize = secondaryLayer.Pagination?.PageSize?.Value;

if (_options.IncludeTotalResourceCount && _request.IsCollection && pageSize != null)
{
await RetrieveResourceCountForNonPrimaryEndpointAsync(id, (HasManyAttribute)_request.Relationship, cancellationToken);

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

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

TResource? primaryResource = primaryResources.SingleOrDefault();
AssertPrimaryResourceExists(primaryResource);

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

if (rightValue is ICollection rightResources && secondaryLayer.Pagination?.PageSize?.Value == rightResources.Count)
if (rightValue is IEnumerable rightResources)
{
_paginationContext.IsPageFull = true;
int resourceCount = CollectionConverter.Instance.GetCount(rightResources);

if (pageSize == null)
{
_paginationContext.TotalResourceCount = resourceCount;
}
else if (pageSize == resourceCount)
{
_paginationContext.IsPageFull = true;
}
}

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

ArgumentNullException.ThrowIfNull(relationshipName);
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);
AssertHasRelationship(_request.Relationship, relationshipName);
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);
AssertSecondaryResourceTypeInJsonApiRequestIsNotNull(_request.SecondaryResourceType);

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

if (_options.IncludeTotalResourceCount && _request.IsCollection)
QueryLayer secondaryLayer = _queryLayerComposer.ComposeSecondaryLayerForRelationship(_request.SecondaryResourceType);
QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResourceType, id, _request.Relationship);
int? pageSize = secondaryLayer.Pagination?.PageSize?.Value;

if (_options.IncludeTotalResourceCount && _request.IsCollection && pageSize != null)
{
await RetrieveResourceCountForNonPrimaryEndpointAsync(id, (HasManyAttribute)_request.Relationship, cancellationToken);

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

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

TResource? primaryResource = primaryResources.SingleOrDefault();
AssertPrimaryResourceExists(primaryResource);

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

if (rightValue is ICollection rightResources && secondaryLayer.Pagination?.PageSize?.Value == rightResources.Count)
if (rightValue is IEnumerable rightResources)
{
_paginationContext.IsPageFull = true;
int resourceCount = CollectionConverter.Instance.GetCount(rightResources);

if (pageSize == null)
{
_paginationContext.TotalResourceCount = resourceCount;
}
else if (pageSize == resourceCount)
{
_paginationContext.IsPageFull = true;
}
}

return rightValue;
Expand Down Expand Up @@ -677,6 +706,16 @@ private void AssertPrimaryResourceTypeInJsonApiRequestIsNotNull([SysNotNull] Res
}
}

[AssertionMethod]
private void AssertSecondaryResourceTypeInJsonApiRequestIsNotNull([SysNotNull] ResourceType? resourceType)
{
if (resourceType == null)
{
throw new InvalidOperationException(
$"Expected {nameof(IJsonApiRequest)}.{nameof(IJsonApiRequest.SecondaryResourceType)} not to be null at this point.");
}
}

[AssertionMethod]
private void AssertRelationshipInJsonApiRequestIsNotNull([SysNotNull] RelationshipAttribute? relationship)
{
Expand Down
6 changes: 1 addition & 5 deletions test/DapperTests/IntegrationTests/DapperTestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,7 @@ private WebApplicationFactory<TodoItem> CreateFactory()
builder.ConfigureServices(services =>
{
services.Replace(ServiceDescriptor.Singleton<TimeProvider>(new FrozenTimeProvider(FrozenTime)));

ServiceDescriptor scopedCaptureStore = services.Single(descriptor => descriptor.ImplementationType == typeof(SqlCaptureStore));
services.Remove(scopedCaptureStore);

services.AddSingleton<SqlCaptureStore>();
services.Replace(ServiceDescriptor.Singleton<SqlCaptureStore, SqlCaptureStore>());
});
});
}
Expand Down
Loading
Loading