diff --git a/src/JsonApiDotNetCore/Queries/IPaginationContext.cs b/src/JsonApiDotNetCore/Queries/IPaginationContext.cs
index 9db3a24c9f..e80199de7c 100644
--- a/src/JsonApiDotNetCore/Queries/IPaginationContext.cs
+++ b/src/JsonApiDotNetCore/Queries/IPaginationContext.cs
@@ -19,6 +19,12 @@ public interface IPaginationContext
///
PageSize PageSize { get; set; }
+ ///
+ /// Indicates whether the number of resources on the current page equals the page size.
+ /// When true, a subsequent page might exist (assuming is unknown).
+ ///
+ bool IsPageFull { get; set; }
+
///
/// The total number of resources.
/// null when is set to false.
diff --git a/src/JsonApiDotNetCore/Queries/PaginationContext.cs b/src/JsonApiDotNetCore/Queries/PaginationContext.cs
index fbdd8ad453..0ca1e25076 100644
--- a/src/JsonApiDotNetCore/Queries/PaginationContext.cs
+++ b/src/JsonApiDotNetCore/Queries/PaginationContext.cs
@@ -12,6 +12,9 @@ internal sealed class PaginationContext : IPaginationContext
///
public PageSize PageSize { get; set; }
+ ///
+ public bool IsPageFull { get; set; }
+
///
public int? TotalResourceCount { get; set; }
diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs
index 310f4cd87f..a9ef383e49 100644
--- a/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs
+++ b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs
@@ -46,15 +46,17 @@ public virtual void Read(string parameterName, StringValues parameterValue)
private object GetQueryableHandler(string parameterName)
{
- if (_request.Kind != EndpointKind.Primary)
+ var resourceType = _request.PrimaryResource.ResourceType;
+ var handler = _resourceDefinitionAccessor.GetQueryableHandlerForQueryStringParameter(resourceType, parameterName);
+
+ if (handler != null && _request.Kind != EndpointKind.Primary)
{
throw new InvalidQueryStringParameterException(parameterName,
"Custom query string parameters cannot be used on nested resource endpoints.",
$"Query string parameter '{parameterName}' cannot be used on a nested resource endpoint.");
}
- var resourceType = _request.PrimaryResource.ResourceType;
- return _resourceDefinitionAccessor.GetQueryableHandlerForQueryStringParameter(resourceType, parameterName);
+ return handler;
}
///
diff --git a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs
index 802bfe0fc1..4e0cb1011f 100644
--- a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs
+++ b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs
@@ -5,6 +5,8 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
+using JsonApiDotNetCore.Queries.Expressions;
+using JsonApiDotNetCore.Queries.Internal.Parsing;
using JsonApiDotNetCore.QueryStrings;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
@@ -15,6 +17,9 @@ namespace JsonApiDotNetCore.Serialization.Building
{
public class LinkBuilder : ILinkBuilder
{
+ private const string _pageSizeParameterName = "page[size]";
+ private const string _pageNumberParameterName = "page[number]";
+
private readonly IResourceContextProvider _provider;
private readonly IRequestQueryStringAccessor _queryStringAccessor;
private readonly IJsonApiOptions _options;
@@ -42,10 +47,10 @@ public TopLevelLinks GetTopLevelLinks()
TopLevelLinks topLevelLinks = null;
if (ShouldAddTopLevelLink(resourceContext, LinkTypes.Self))
{
- topLevelLinks = new TopLevelLinks { Self = GetSelfTopLevelLink(resourceContext) };
+ topLevelLinks = new TopLevelLinks {Self = GetSelfTopLevelLink(resourceContext, null)};
}
- if (ShouldAddTopLevelLink(resourceContext, LinkTypes.Paging) && _paginationContext.PageSize != null)
+ if (ShouldAddTopLevelLink(resourceContext, LinkTypes.Paging) && _paginationContext.PageSize != null && _request.IsCollection)
{
SetPageLinks(resourceContext, topLevelLinks ??= new TopLevelLinks());
}
@@ -70,36 +75,38 @@ private bool ShouldAddTopLevelLink(ResourceContext resourceContext, LinkTypes li
private void SetPageLinks(ResourceContext resourceContext, TopLevelLinks links)
{
- if (_paginationContext.PageNumber.OneBasedValue > 1)
+ links.First = GetPageLink(resourceContext, 1, _paginationContext.PageSize);
+
+ if (_paginationContext.TotalPageCount > 0)
{
- links.Prev = GetPageLink(resourceContext, _paginationContext.PageNumber.OneBasedValue - 1, _paginationContext.PageSize);
+ links.Last = GetPageLink(resourceContext, _paginationContext.TotalPageCount.Value, _paginationContext.PageSize);
}
- if (_paginationContext.PageNumber.OneBasedValue < _paginationContext.TotalPageCount)
+ if (_paginationContext.PageNumber.OneBasedValue > 1)
{
- links.Next = GetPageLink(resourceContext, _paginationContext.PageNumber.OneBasedValue + 1, _paginationContext.PageSize);
+ links.Prev = GetPageLink(resourceContext, _paginationContext.PageNumber.OneBasedValue - 1, _paginationContext.PageSize);
}
- if (_paginationContext.TotalPageCount > 0)
+ bool hasNextPage = _paginationContext.PageNumber.OneBasedValue < _paginationContext.TotalPageCount;
+ bool possiblyHasNextPage = _paginationContext.TotalPageCount == null && _paginationContext.IsPageFull;
+
+ if (hasNextPage || possiblyHasNextPage)
{
- links.Self = GetPageLink(resourceContext, _paginationContext.PageNumber.OneBasedValue, _paginationContext.PageSize);
- links.First = GetPageLink(resourceContext, 1, _paginationContext.PageSize);
- links.Last = GetPageLink(resourceContext, _paginationContext.TotalPageCount.Value, _paginationContext.PageSize);
+ links.Next = GetPageLink(resourceContext, _paginationContext.PageNumber.OneBasedValue + 1, _paginationContext.PageSize);
}
}
- private string GetSelfTopLevelLink(ResourceContext resourceContext)
+ private string GetSelfTopLevelLink(ResourceContext resourceContext, Action> queryStringUpdateAction)
{
var builder = new StringBuilder();
builder.Append(_request.BasePath);
builder.Append("/");
builder.Append(resourceContext.PublicName);
- string resourceId = _request.PrimaryId;
- if (resourceId != null)
+ if (_request.PrimaryId != null)
{
builder.Append("/");
- builder.Append(resourceId);
+ builder.Append(_request.PrimaryId);
}
if (_request.Relationship != null)
@@ -108,49 +115,102 @@ private string GetSelfTopLevelLink(ResourceContext resourceContext)
builder.Append(_request.Relationship.PublicName);
}
- builder.Append(DecodeSpecialCharacters(_queryStringAccessor.QueryString.Value));
+ string queryString = BuildQueryString(queryStringUpdateAction);
+ builder.Append(queryString);
return builder.ToString();
}
+ private string BuildQueryString(Action> updateAction)
+ {
+ var parameters = _queryStringAccessor.Query.ToDictionary(pair => pair.Key, pair => pair.Value.ToString());
+ updateAction?.Invoke(parameters);
+ string queryString = QueryString.Create(parameters).Value;
+
+ return DecodeSpecialCharacters(queryString);
+ }
+
+ private static string DecodeSpecialCharacters(string uri)
+ {
+ return uri.Replace("%5B", "[").Replace("%5D", "]").Replace("%27", "'").Replace("%3A", ":");
+ }
+
private string GetPageLink(ResourceContext resourceContext, int pageOffset, PageSize pageSize)
{
- string queryString = BuildQueryString(parameters =>
+ return GetSelfTopLevelLink(resourceContext, parameters =>
{
- if (pageSize == null || pageSize.Equals(_options.DefaultPageSize))
+ var existingPageSizeParameterValue = parameters.ContainsKey(_pageSizeParameterName)
+ ? parameters[_pageSizeParameterName]
+ : null;
+
+ PageSize newTopPageSize = Equals(pageSize, _options.DefaultPageSize) ? null : pageSize;
+
+ string newPageSizeParameterValue = ChangeTopPageSize(existingPageSizeParameterValue, newTopPageSize);
+ if (newPageSizeParameterValue == null)
{
- parameters.Remove("page[size]");
+ parameters.Remove(_pageSizeParameterName);
}
else
{
- parameters["page[size]"] = pageSize.ToString();
+ parameters[_pageSizeParameterName] = newPageSizeParameterValue;
}
if (pageOffset == 1)
{
- parameters.Remove("page[number]");
+ parameters.Remove(_pageNumberParameterName);
}
else
{
- parameters["page[number]"] = pageOffset.ToString();
+ parameters[_pageNumberParameterName] = pageOffset.ToString();
}
});
-
- return $"{_request.BasePath}/{resourceContext.PublicName}" + queryString;
}
- private string BuildQueryString(Action> updateAction)
+ private string ChangeTopPageSize(string pageSizeParameterValue, PageSize topPageSize)
{
- var parameters = _queryStringAccessor.Query.ToDictionary(pair => pair.Key, pair => pair.Value.ToString());
- updateAction(parameters);
- string queryString = QueryString.Create(parameters).Value;
+ var elements = ParsePageSizeExpression(pageSizeParameterValue);
+ var elementInTopScopeIndex = elements.FindIndex(expression => expression.Scope == null);
- return DecodeSpecialCharacters(queryString);
+ if (topPageSize != null)
+ {
+ var topPageSizeElement = new PaginationElementQueryStringValueExpression(null, topPageSize.Value);
+
+ if (elementInTopScopeIndex != -1)
+ {
+ elements[elementInTopScopeIndex] = topPageSizeElement;
+ }
+ else
+ {
+ elements.Insert(0, topPageSizeElement);
+ }
+ }
+ else
+ {
+ if (elementInTopScopeIndex != -1)
+ {
+ elements.RemoveAt(elementInTopScopeIndex);
+ }
+ }
+
+ var parameterValue = string.Join(',',
+ elements.Select(expression => expression.Scope == null ? expression.Value.ToString() : $"{expression.Scope}:{expression.Value}"));
+
+ return parameterValue == string.Empty ? null : parameterValue;
}
- private static string DecodeSpecialCharacters(string uri)
+ private List ParsePageSizeExpression(string pageSizeParameterValue)
{
- return uri.Replace("%5B", "[").Replace("%5D", "]").Replace("%27", "'");
+ if (pageSizeParameterValue == null)
+ {
+ return new List();
+ }
+
+ var requestResource = _request.SecondaryResource ?? _request.PrimaryResource;
+
+ var parser = new PaginationParser(_provider);
+ var paginationExpression = parser.Parse(pageSizeParameterValue, requestResource);
+
+ return new List(paginationExpression.Elements);
}
///
diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs
index e72377f877..0c273a8015 100644
--- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs
+++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -119,6 +120,11 @@ public virtual async Task> GetAsync()
{
var topFilter = _queryLayerComposer.GetTopFilter();
_paginationContext.TotalResourceCount = await _repository.CountAsync(topFilter);
+
+ if (_paginationContext.TotalResourceCount == 0)
+ {
+ return Array.Empty();
+ }
}
var queryLayer = _queryLayerComposer.Compose(_request.PrimaryResource);
@@ -130,6 +136,11 @@ public virtual async Task> GetAsync()
return _hookExecutor.OnReturn(resources, ResourcePipeline.Get).ToArray();
}
+ if (queryLayer.Pagination?.PageSize != null && queryLayer.Pagination.PageSize.Value == resources.Count)
+ {
+ _paginationContext.IsPageFull = true;
+ }
+
return resources;
}
@@ -233,6 +244,14 @@ public virtual async Task