Biarity / Sieve

⚗️ Clean & extensible Sorting, Filtering, and Pagination for ASP.NET Core
Other
1.22k stars 132 forks source link

Add paging info to Apply result #53

Open jakoss opened 5 years ago

jakoss commented 5 years ago

Hi,

Have you considered adding some kind of paging info on Apply result? It's common pattern that we need Page, PageSize and TotalCount of filtered items. The only option now is to call Apply 2 times (one with turned off paging) to get TotalCount, but that's highly unefficient since there is no caching of expressions and reflections in library (that might be another case of issue though).

I think the api could look like:

PagingInfo pagingInfo = new PagingInfo();
var query = query.Apply(sieveModel, query, pagingInfo);

I don't really like the out object passed to function. But the alternative is maybe to return object from Apply? But that's pretty big breaking change. Please let me know what you think.

EDIT: I'm more than happy to create PR with this as soon as we can figure out how API could look like :)

skolmer commented 5 years ago

We had the same requirement and just created an extension method:

    public static class ISieveProcessorExtensions
    {
        public static async Task<PagedResult<T>> GetPagedAsync<T>(this ISieveProcessor sieveProcessor, IQueryable<T> query, SieveModel sieveModel = null) where T : class
        {
            var result = new PagedResult<T>();

            var (pagedQuery, page, pageSize, rowCount, pageCount) = await GetPagedResultAsync(sieveProcessor, query, sieveModel);

            result.CurrentPage = page;
            result.PageSize = pageSize;
            result.RowCount = rowCount;
            result.PageCount = pageCount;

            result.Results = await pagedQuery.ToListAsync();

            return result;
        }

        private static async Task<(IQueryable<T> pagedQuery, int page, int pageSize, int rowCount, int pageCount)> GetPagedResultAsync<T>(ISieveProcessor sieveProcessor, IQueryable<T> query, SieveModel sieveModel = null) where T : class
        {
            var page = sieveModel?.Page ?? 1;
            var pageSize = sieveModel?.PageSize ?? 50;

            if (sieveModel != null)
            {
                // apply pagination in a later step
                query = sieveProcessor.Apply(sieveModel, query, applyPagination: false);
            }

            var rowCount = await query.CountAsync();

            var pageCount = (int)Math.Ceiling((double)rowCount / pageSize);

            var skip = (page - 1) * pageSize;
            var pagedQuery = query.Skip(skip).Take(pageSize);

            return (pagedQuery, page, pageSize, rowCount, pageCount);
        }
    }

    public class PagedResult<T> where T : class
    {
        public IList<T> Results { get; set; }
        public int CurrentPage { get; set; }
        public int PageCount { get; set; }
        public int PageSize { get; set; }
        public long RowCount { get; set; }

        public PagedResult()
        {
            Results = new List<T>();
        }
    }

Hope this helps :)

jakoss commented 5 years ago

We did paging ourselves. I just wanted to make some pr here 😁

emouawad commented 5 years ago

@skolmer Thanks - this is helpful 😄

ITDancer13 commented 3 years ago

I like the idea as I had to do the same for my projects. Added it to plan for v3.0

tjakopan commented 3 years ago

Did something similar to the example above.

    public class PagedResult<T>
    {
        public PagedResult(int page, int pageCount, int pageSize, long rowCount, IQueryable<T> results)
        {
            Page = page;
            PageCount = pageCount;
            PageSize = pageSize;
            RowCount = rowCount;
            Results = results;
        }

        public int Page { get; }

        public int PageCount { get; }

        public int PageSize { get; }

        public long RowCount { get; }

        public IQueryable<T> Results { get; }
    }

    public interface IDataSieveProcessor : ISieveProcessor
    {
        public Task<PagedResult<T>> ApplyAsync<T>(
            SieveModel sieveModel,
            IQueryable<T> source,
            object[]? dataForCustomMethods = null,
            bool applyFiltering = true,
            bool applySorting = true
        );
    }

    public class DataSieveProcessor : SieveProcessor, IDataSieveProcessor
    {
        private readonly IOptions<SieveOptions> _options;

        public DataSieveProcessor(IOptions<SieveOptions> options) : base(options)
        {
            _options = options;
        }

        protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
        {
            mapper.Property<Person>(person => person.FirstName)
                .CanFilter()
                .CanSort();
            mapper.Property<Person>(person => person.LastName)
                .CanFilter()
                .CanSort();
            return mapper;
        }

        public async Task<PagedResult<T>> ApplyAsync<T>(
            SieveModel sieveModel,
            IQueryable<T> source,
            object[]? dataForCustomMethods = null,
            bool applyFiltering = true,
            bool applySorting = true
        )
        {
            var result = Apply(sieveModel, source, dataForCustomMethods, applyFiltering, applySorting,
                false);
            var page = sieveModel.Page ?? 1;
            var pageSize = sieveModel.PageSize ?? _options.Value.DefaultPageSize;
            var rowCount = await result.LongCountAsync();
            var pageCount = (int)Math.Ceiling((double)rowCount / pageSize);
            result = Apply(sieveModel, result, dataForCustomMethods, false, false);
            return new PagedResult<T>(page, pageCount, pageSize, rowCount, result);
        }
    }

Doing this in Startup:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.Configure<SieveOptions>(Configuration.GetSection("Sieve"));
            services.AddScoped<IDataSieveProcessor, DataSieveProcessor>();
        }

I can probably find some time to contribute, make it more generic and part of the library via pull request, if there's any interest.

I wanted to keep IQueryable in the PagedResult so I can do custom mapping afterwards, to another class (rest api representation for example).