Search  
Always will be ready notify the world about expectations as easy as possible: job change page
Articles
18 января 2023 г.

Using a сustom PagedList class for Generic Pagination in .NET Core

Author:
Savindu Bandara
Source:
Views:
4059

What is pagination

So you may have already used this one, but you may be wondering what this pagination 😀. So as in figure 1, pagination helps you break a large number of datasets into smaller pages. For example, a storybook contains 1000 pages. Why does it have 1000 pages instead of 1? To ease the reading. Same thing in here as well. Imagine you are dealing with a dataset that contains 2 million records. But what happens if you try to retrieve all the datasets at once? It may hit your performance, waste your data, and even crash your application. For that, pagination is the Best Solution.

Pagination allows you to select a specific page from a range of pages. In this, we can define:

  • Current Page — (The current page where the user is in. i.e. 2nd page out of 50)
  • Page size — (Number of items per page (i.e. 100 total data and ten items per page))
  • Total pages — (Total number of pages. i.e. (100 data and ten each page. So 100/10 = 10 Pages))
  • Total Count — Defined as the total number of data in the dataset.

The traditional method of Pagination in SQL

So as in traditional SQL, we can use OFFSET and FETCH keywords and select keywords to paginate. Please refer to the following snippet and diagram.

SELECT FruitName, Price
FROM SampleFruits
ORDER BY Price
OFFSET 5 ROWS FETCH NEXT 6 ROWS ONLY

In the above example, you can see that offset means skipped rows, and the Next keyword takes the next six after skipping five records. And you can use the OFFSET command without the FETCH keyword as well.

How to use pagination in .NET Core with Entity Framework?

Entity Framework is the most popular and most reliable ORM.NET framework. By entity framework, you can work with. The below example defines sample HTTP Get request with pagination information attached as query parameters.

GET https://localhost:5001/pictures/paging?page=100000&size=10
Accept: application/json

The page is defined as 100000, and the page size is 10. If we mapped this GET Request to the Repository in the back end, we could come up with this.

public async Task<List<Users>> GetAllUsers(int page, int pageSize)
{
    var query = db
        .Users
        .OrderBy(user => user.Id)
        .Skip((page - 1) * pageSize)
        .Take(pageSize);
}

This method can be used for paging By Page and Size. The cursor method is different, but I am talking about the first method. But what is the problem here? This is only specific to one Repository, and if there is another place that we need to do this pagination, we have to duplicate the code again 🤔. Imagine there are hundreds of places that need pagination. So this is the not optimal way of doing this. We can enhance this pagination by adding a PagedList class for all types of paginations where we can take any data type as input and return the paged List as the user requested.

The Solution 😃

Creating PageList class as a generic class will help sort out this issue. Let’s try to implement this one step by step.

1. Create a PagedList Class inside your project directory (If there is a Helpers class directory, please add this there).

public class PagedList<T> : List<T>
{
}

We should define PagedList class as a <T> Because that is defined for generic (Means where T is a class) and should implement List <T> as an implementation of List.

2. Add model Properties inside the class

public int CurrentPage { get; set; }
public int TotalPages { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }

This model will help you deal with the Repository, and we should pass data to this class from the Repository. So we can create a different method here, but I would like to use a constructor with parameters to sort out that.

3. Implement a constructor in PagedList class. Use ctorf annotation to generate automatic constructor to the model class and adjust that as b.

public PagedList(IEnumerable<T> currentPage, int count, int pageNumber, int pageSize)
{
    CurrentPage = pageNumber;
    TotalPages = (int) Math.Ceiling(count / (double) pageSize);
    PageSize = pageSize;
    TotalCount = count;
    AddRange(currentPage);
}

CurrentPage can be assigned to the pageNumber coming from the request, and TotalPages can be calculated from count dividing by the pageSize. And here special AddRange(currentPage) method is used. Because currentPage is IEumarable<T> and the actual paginated result set. So we need to push that range before we return the paginated result. You may get this when we implement this.

4. Implement a CreateAsync() method as below, which takes the source of the collection and Page Number and Page Size.

public static async Task<PagedList<T>> CreateAsync(IQueryable<T> source, int pageNumber, int pageSize)
{
    var count = await source.CountAsync();
    var items = await source.Skip((pageNumber)*pageSize).Take(pageSize).ToListAsync();

    return new PagedList<T>(items, count, pageNumber, pageSize);
}

Again the method is defined as async because it deals with the dataset. So getting count from the source and var items = … describes the Skip() and Take() methods. Parameters are coming by the method. And there is an Object return by calling the above-defined constructor. Now you may clarify why we implemented the constructor.

public class PagedList<T> : List<T>
{
    public PagedList(IEnumerable<T> currentPage, int count, int pageNumber, int pageSize)
    {
        CurrentPage = pageNumber;
        TotalPages = (int) Math.Ceiling(count / (double) pageSize);
        PageSize = pageSize;
        TotalCount = count;
        AddRange(currentPage);
    }

    public int CurrentPage { get; set; }
    public int TotalPages { get; set; }
    public int PageSize { get; set; }
    public int TotalCount { get; set; }

    public static async Task<PagedList<T>> CreateAsync(IQueryable<T> source, int pageNumber, int pageSize)
    {
        var count = await source.CountAsync();
        var items = await source.Skip((pageNumber)*pageSize).Take(pageSize).ToListAsync();
   
        return new PagedList<T>(items, count, pageNumber, pageSize);
    }
}

5. Implement a UserParams class to get pageNumber and pageSize.

public class UserParams
{
    private const int MaxPageSize = 50;
    public int PageNumber { get; set; } = 1;
    private int _pageSize = 10;
 
    public int PageSize
    {
        get => _pageSize;
        set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value;
    }
}

MaxPageSize is hardcoded here, so if you need to add that in query param, you can add that. PageNumber also default number should add as you will get an error if the PageNumber not provided (Assume that the method should work page load state. So method with default — means without parameters). And page size private variable default value 10. but You can change that. And MaxPageSize also should be increased depending on PageSize.

6. Implement the Repository which will provide you with the paginated result.

public async Task<PagedList<MemberDto>> GetMembersAsync(UserParams userParams)
{
    var query = _dataContext.Users.AsQueryable();

    return await PagedList<MemberDto>.CreateAsync(query.ProjectTo<MemberDto>(_mapper.ConfigurationProvider).AsNoTracking(), userParams.PageNumber, userParams.PageSize);
}

We need to take the Users from the data context as queriable. Else we cannot use Skip and take methods inside CreateAsync. PageNumber and PageSize are coming inside user params to provide them with to method. AsNoTracking() method was also added to improve performance furthermore. By default, EF Core tracks the history of the context. We have the information now what to receive from where and from where to start + what should skip. Then we do not need any tracking information.

7. Implement in the controller.

[HttpGet]
public async Task<IActionResult> GetUsers([FromQuery]UserParams userParams)
{
    var users = await _userRepository.GetMembersAsync(userParams);
   
    return Ok(users);
}

That’s it. Generic PagedList class has been implemented 😃. Now you can test the API via Postman or Swagger.

So the first figure is without any parameters ( No PageNumber and No Pagesize). It will automatically trigger the default values that I have added to the UserParams class.

Then I will add a Page number as one and PageSize as 3.

Response body — With three users

If I hit the next page (Page number as 2), you can see the response (UserID) starting from 4.

Response body — Second Page

So that’s it. Now we have implemented a Generic Pagination class that we can use in any data context. 😁

This is additional if you need to add your pagination information to the response header. You can do that as well. You can write an extension method for that.

1. Create PaginationHeader class under your Helper class directory.

public class PaginationHeader
{
    public PaginationHeader(int currentPage, int itemsPerPage, int totalItems, int totalPages)
    {
        CurrentPage = currentPage;
        ItemsPerPage = itemsPerPage;
        TotalItems = totalItems;
        TotalPages = totalPages;
    }
   
    public int CurrentPage { get; set; }
    public int ItemsPerPage { get; set; }
    public int TotalItems { get; set; }
    public int TotalPages { get; set; }
}

This metadata will be used when returning your result along with the paginated result.

2. Create HttpExtensions Class under your Extensions Directory.

public static class HttpExtensions
{
    public static void AddPaginationHeader(this HttpResponse response, int currentPage, int itemsPerPage,int totalItems, int totalPages)
    { 
        var paginationHeader = new PaginationHeader(currentPage, itemsPerPage, totalItems, totalPages);
        var options = new JsonSerializerOptions()
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        response.Headers.Add("Pagination", JsonSerializer.Serialize(paginationHeader, options));
        response.Headers.Add("Access-Control-Expose-Headers", "Pagination");
    }
}

Note: This is an extension method (using this keyword )so you can directly call this method under HttpResponse reference.

So you can modify your controller along with this to add the response header information.

Response.AddPaginationHeader(users.CurrentPage, users.PageSize, users.TotalCount, users.TotalPages);

users variable is a type of PagedList of MemberDto. So inside PagedList, we have CurrentPage, PageSize, TotalCount and TotalPages as we added them into the PagedList class then we can use them for AddPaginationHeader method.

I have my Angular front end application, and I have integrated this one and got the following header results.

Pagination information appeared in Response Headers

So that’s quite it 😀. I hope this will be beneficial for your implementations as a .NET developer, and you are dealing with pagination with a large number of entities.

That’s all for today. Keep in touch for more articles.

Till then, stay safe and Happy coding 😁❤

Similar
Jul 5
Author: David
The .NET Generic Host is a feature which sets up some convenient patterns for an application including those for dependency injection (DI), logging, and configuration. It was originally named Web Host and intended for Web scenarios like ASP.NET Core applications...
Jan 7
Author: Sebastian Stupak
Few days ago I stopped myself while writing code. I wrote my LINQ filtering wrong. items.Where(x => x > 0).Any(); (Obviously, it’s a pseudo code) I realized my mistake immediately, changed the code, and went on with my day. Then...
May 10
Author: Matt Bentley
A guide to implementing the Unit of Work pattern in an Entity Framework .NET application and using it to drive additional behaviours. In this article we will explore how the Unit of Work pattern can be used to improve data...
Sep 5, 2023
Author: Edson Moisinho
Simplifying data transport in C#. In modern C# development, data transport objects (DTOs) play a crucial role in exchanging information between different layers of an application, such as between a client and a server, and traditionally, developers have used classes...
Send message
Type
Email
Your name
*Message