nyxthulhu
nyxthulhu

Reputation: 9752

Pagination in a .NET Core API Project

I am trying to implement pagination on .NET Core RESTful API's (with EF).

Rather than re-inventing the wheel, I was hoping there was a way to either use a generic function that could hook into the API handler and intercept requests to apply them to EF Query results or something built into .NET Core that I am un-aware of. Can anyone point me in the general direction of a library that they are aware of that does this in .NET Core

The way I would previously do this (and have done in non .NET Core apps) is to make a function that I have to physically add the parameters to the controller function (pageSize, pageNumber) which does get tedious (and I think a tad untidy) to add these two parameters to every single function.

Upvotes: 17

Views: 21485

Answers (2)

Sir Rufo
Sir Rufo

Reputation: 19106

There is no built-in feature for pagination, and if there was, you would not 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.

Upvotes: 22

jackal
jackal

Reputation: 1219

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);
}
// ...

Upvotes: 6

Related Questions