I'm not sure about "best practises" - there are many different ways to achieve the same thing, with varying degrees of complexity.
What I tend to do is return a class which encapsulates the results:
public class EntityList<TEntity>
{
public int TotalCount { get; set; }
public int FilteredCount { get; set; }
public IReadOnlyList<TEntity> PageData { get; set; }
}
public EntityList<TEntity> GetAll(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
IEnumerable<Expression<Func<TEntity, object>>> includes = null,
int? page = null, int? pageSize = null)
{
var query = context.Set<TEntity>().AsQueryable();
if (includes != null)
{
query = includes.Aggregate(query, (current, include) => current.Include(include));
}
int totalCount = query.Count();
int filteredCount = totalCount;
if (filter != null)
{
query = query.AsExpandable().Where(filter);
filteredCount = query.Count();
}
if (orderBy != null)
{
query = orderBy(query);
}
if (page != null && pageSize != null)
{
query = query.Skip((page.Value - 1) * pageSize.Value).Take(pageSize.Value);
}
var pageData = query.ToList();
return new EntityList<TEntity>
{
TotalCount = totalCount,
FilteredCount = filteredCount,
PageData = pageData,
};
}
Alternatively, OData allows you to request the total number of records to be returned in the response:
Query options overview - OData | Microsoft Docs[
^]
And GraphQL takes a slightly different approach:
Pagination | GraphQL[
^]