Description
Paging links are currently implemented in two ways:
- If
options.IncludeTotalResourceCount
is enabled, we fetchCOUNT(*)
before retrieving the actual data. This enables the link renderer to know the total number of pages, so it can render the Next/Last links reliably. - If this option is not enabled, the link renderer cannot know the total number of pages, so it switches to best-effort mode. This means it never renders Last and renders Next only when the current page is full. Note this may result in a Next link to an empty page.
For secondary endpoints, we currently have no way to fetch COUNT(*)
, so the link renderer always uses best-effort mode. The reason is that our query composition layer does not support retrieving nested values that do not map into a resource object.
Example for primary endpoint /articles?filter...
:
var query =
from article in DbContext.Articles
where (filter)
select article
var count = query.Count();
Example for secondary endpoint /blogs/1/articles?filter...
:
var query =
from blog in DbContext.Blogs
where blog.Id == 1
select new Blog(
Id = blog.Id,
Articles = blog.Articles.Where(filter) // where to put the count?
);
var count = ?
There is no way to determine the total number of articles in the second example: query.Count()
would always return 1. query.Single().Articles.Count()
would fetch all articles first, then determine count in-memory (which is a no-go for large tables).
The proposed solution (to determine the total count on secondary endpoints) is to turn the query upside-down. By using the inverse of the relationship. So instead of using the relationship Blog.Articles
, we use its inverse, which is Article.Blog
.
Example for secondary endpoint /blogs/1/articles?filter...
:
var query =
from article in DbContext.Articles
where article.Blog.Id == 1
where (filter)
select article;
var count = query.Count();
Notes:
- If the inverse relationship is unavailable, there's nothing we can do to improve the current behavior. By default, we use the underlying EF Core model to find the inverse.
- If another data source is used, developers need to implement a custom
IInverseNavigationResolver
. - This proposal solely affects logic to determine the total resource count. The actual fetching of data remains as-is.
- This proposal only applies to to-many relationships, so we'll need to tackle many-to-many relationships too.
- This requires a resource service to use a repository for a different resource type. For example:
ResourceService<Blog>
delegates toResourceRepository<Article>
. We can use our existingIResourceRepositoryAccessor
for that. IResourceDefinition.OnApplyFilter
must be called on the secondary resource type, possibly on the primary resource type too.