Pagination in a .NET Core API Project

Here's a ready-to-use code based on Sir Rufo's answer:

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace ProjectName.Utilities
{
    public static class Extensions
    {
        public static async Task<PaginatedResult<T>> paginate<T>(this IQueryable<T> source,
                                                int pageSize, int pageNumber)
        {
            return await new PaginatedResult<T>(pageNumber, pageSize).paginate(source);
        }
    }

    public class PaginatedResult<T> : ActionResult
    {
        private const int defaultPageSize = 20;
        private const int maxPageSize = 50;

        public int total { get; private set; }
        public int limit { get; private set; }
        public int page { get; private set; }
        public List<T> objects { get; private set; }

        internal PaginatedResult(int pageNumber, int pageSize = defaultPageSize)
        {
            limit = pageSize;
            page = pageNumber;

            if (limit < 0 || limit > maxPageSize)
            {
                limit = defaultPageSize;
            }
            if (pageNumber < 0)
            {
                page = 0;
            }
        }

        internal async Task<PaginatedResult<T>> paginate(IQueryable<T> queryable)
        {
            total = queryable.Count();

            if (limit > total)
            {
                limit = total;
                page = 0;
            }

            int skip = page * limit;
            if (skip + limit > total)
            {
                skip = total - limit;
                page = total / limit - 1;
            }

            objects = await queryable.Skip(skip).Take(limit).ToListAsync();
            return this;
        }
    }
}


And in your controller:

// ...
[HttpGet]
public async Task<ActionResult<PaginatedResult<MyDataType>>> getMyData(int pageSize = 20,
                                                                       int pageNumber = 0)
{
    return await _context.myData.AsNoTracking().paginate(pageSize, pageNumber);
}
// ...

There is no built-in feature far pagination, and if you wont like it. Imagine a controller method returning 1.000.000 results for pagination just to pick 10 from them. It is up to you to implement the pagination.

The tedious and untidy controller methods like

public class FooController : Controller
{
    public IEnumerable<Foo> GetAll( 
        string Filter, 
        string Whatever, 
        ..., 
        int pageNumber = 1, 
        int pageSize = 20 ) 
    { ... }
}

can be reorganized to

public class FooController : Controller
{
    public IEnumerable<Foo> GetAll( GetAllArgs args ) 
    {
        IQueryable<Foo> query = ...

        return query.Paginate( args ).ToList();  
    }

    public class GetAllArgs : QueryArgsBase
    {
        public string Filter { get; set; }
        public string Whatever { get; set; }
    }
}

public interface IPaginationInfo
{ 
    int PageNumber { get; }
    int PageSize { get; }
}

public abstract class QueryArgsBase : IPaginationInfo
{
    public int PageNumber { get; set; } = 1;
    public int PageSize { get; set; } = 20;
}

public static class QueryableExtensions
{
    public static IQueryable<T> Paginate<T>( 
        this IQueryable<T> source, 
        IPaginationInfo pagination )
    {
        return source
            .Skip( ( pagination.PageNumber - 1 ) * pagination.PageSize )
            .Take( pagination.PageSize );
    }
}

Change any other controller method to have such an argument class and inherite from QueryArgsBase or implement IPaginationInfo to use the QueryableExtensions.Paginate method.