Skip to content

Improved paging links on secondary endpoint using inverse relationship #1010

Closed
@bart-degreed

Description

@bart-degreed

Paging links are currently implemented in two ways:

  1. If options.IncludeTotalResourceCount is enabled, we fetch COUNT(*) 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.
  2. 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 to ResourceRepository<Article>. We can use our existing IResourceRepositoryAccessor for that.
  • IResourceDefinition.OnApplyFilter must be called on the secondary resource type, possibly on the primary resource type too.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions