From 69b423b56567ae30c33f6441ba30e1e9bb7c7dc8 Mon Sep 17 00:00:00 2001 From: Neil Brommer Date: Sat, 8 Jul 2023 16:50:21 -0700 Subject: [PATCH] Add post about C# pagination --- src/posts/CsPaginationTools.md | 329 +++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 src/posts/CsPaginationTools.md diff --git a/src/posts/CsPaginationTools.md b/src/posts/CsPaginationTools.md new file mode 100644 index 0000000..e761428 --- /dev/null +++ b/src/posts/CsPaginationTools.md @@ -0,0 +1,329 @@ +--- +title: C# Pagination Tools +description: A couple of classes that make pagination much easier. +tags: [ Programming, C#, Entity Framework ] +--- + +A couple of classes that make pagination much easier. + +There are two core classes: +* `PaginationOptions` contains the the details needed for fetching data +* `PaginatedResults` extends `PaginationOptions` and contains the results and the number of total results. It also contains a bunch of shortcut properties for calculating common pagination values like the total number of pages, the number of the first result on the current page, etc. + +## How To Use It + +```cs +int pageNumber = 1; +int resultsPerPage = 25; +PaginatedResults results = await db.Things + .Where(t => t.RecordInactiveDate == null) + .ToPaginatedResultsAsync(new PaginationOptions(pageNumber, resultsPerPage)); +``` + +## Code + +```cs +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace ASIS.Shared; + +/// Defines how to get paginated data +[DataContract(IsReference = true)] +public class PaginationOptions +{ + /// + /// This constructor exists so that MVC can instantiate the object before mapping is contents + /// + public PaginationOptions() { } + + public PaginationOptions(int pageNumber, int? resultsPerPage) + { + this.PageNumber = pageNumber; + this.ResultsPerPage = resultsPerPage; + } + + private int? _resultsPerPage; + private int _pageNumber; + + /// The number of results per page + [DataMember] + public int? ResultsPerPage + { + get => this._resultsPerPage; + set + { + if (value is not null and <= 0) + throw new ArgumentOutOfRangeException(nameof(this.ResultsPerPage), + $"{this.ResultsPerPage} must be greater than 0"); + + this._resultsPerPage = value; + } + } + + /// + /// The page number to get + /// + /// One indexed + [DataMember] + public int PageNumber + { + get => this._pageNumber; + set + { + if (value <= 0) + throw new ArgumentOutOfRangeException(nameof(this.PageNumber), + $"{this.PageNumber} must be greater than 0"); + + this._pageNumber = value; + } + } +} + +/// +/// An extension of that contains the results from a query and uses +/// them to calculate additional pagination info +/// +/// +/// The type that will contain the data () +/// +/// The data type of the results +[DataContract(IsReference = true)] +public class PaginatedResults : PaginationOptions where T : class +{ + [JsonConstructor] + public PaginatedResults(int pageNumber, int? resultsPerPage, IEnumerable results, + int totalResultsCount) : base(pageNumber, resultsPerPage) + { + this.Results = results; + this.TotalResultsCount = totalResultsCount; + } + + public PaginatedResults(PaginationOptions? paginationOptions, IEnumerable results, + int totalResultsCount) + : base(paginationOptions?.PageNumber ?? 1, paginationOptions?.ResultsPerPage) + { + this.Results = results; + this.TotalResultsCount = totalResultsCount; + } + + /// + /// The total number of results being paginated. This is used for determining where you are + /// in the list. + /// + [DataMember] + public int TotalResultsCount { get; set; } + + /// + /// The number of results on the current page + /// + [DataMember] + public int? CurrentPageResultsCount { get => this.Results.Count(); } + + /// + /// The number of the first result on the current page. This is calculated using + /// , , and . + /// + [DataMember] + public int? FirstResultNumber + { + get + { + if (this.ResultsPerPage is null) + return 1; + + return (this.ResultsPerPage * (this.PageNumber - 1)) + 1; + } + } + + /// + /// The number of the last result on the current page. This is calculated using + /// , , and . + /// + [DataMember] + public int? LastResultNumber + { + get + { + if (this.ResultsPerPage is null) + return this.Results.Count(); + + return (this.ResultsPerPage * (this.PageNumber - 1)) + this.CurrentPageResultsCount; + } + } + + /// + /// The total number of pages based on the and + /// + /// + [DataMember] + public int? LastPageNumber + { + get + { + if (this.ResultsPerPage is null) + return 1; + + return (int)Math.Ceiling(this.TotalResultsCount + / (double)this.ResultsPerPage); + } + } + + /// + /// A shortcut that checks if is equal to 1 + /// + [DataMember] + public bool IsFirstPage { get => this.PageNumber == 1; } + + /// + /// A shortcut that checks if is equal to + /// + /// + [DataMember] + public bool IsLastPage { get => this.PageNumber == this.LastPageNumber; } + + /// A shortcut to check if the current page is not 1 + [DataMember] + public bool HasPreviousPage { get => !this.IsFirstPage; } + + /// + /// A shortcut to check if the current page is not + /// + [DataMember] + public bool HasNextPage { get => !this.IsLastPage; } + + /// + /// The number of the previous page or 1 if + /// + [DataMember] + public int PreviousPageNumber + { + get + { + if (this.IsFirstPage) + return 1; + + return this.PageNumber - 1; + } + } + + /// + /// The number of the next page or if + /// + [DataMember] + public int NextPageNumber + { + get + { + if (this.IsLastPage) + return this.LastPageNumber; + + return this.PageNumber + 1; + } + } + + [DataMember] + public IEnumerable Results { get; set; } +} + +public static class PaginationOptionsExtensionMethods +{ + /// + /// Applies a Skip/Take to the query and returns it + /// + /// Simply returns the unmodified if the is + /// null (i.e. null means don't paginate) + /// + /// + /// The query to paginate + /// The options for paginating the query + public static IQueryable ApplyPaginationOptions(this IQueryable query, + PaginationOptions? options) + { + if (options is null || options.ResultsPerPage is null) + return query; + + return query + .Skip((options.PageNumber - 1) * (int)options.ResultsPerPage) + .Take((int)options.ResultsPerPage); + } + + /// + /// Execute the with the to create + /// a + /// + /// + /// This executes the twice: once with ApplyPaginationOptions + /// and ToList to get the and a second + /// time with Count to get the + /// + /// The result data type + /// + /// The query to execute to get the and + /// + /// + /// + /// The pagination options to apply to the to get the + /// + /// + public static PaginatedResults ToPaginatedResults(this IQueryable query, + PaginationOptions? paginationOptions) + where T : class + { + return new PaginatedResults( + paginationOptions, + query.ApplyPaginationOptions(paginationOptions).ToList(), + query.Count()); + } +} +``` + +This async extension method requires the project it's in to have the Microsoft.EntityFrameworkCore installed: + +```cs +using ASIS.Shared; +using Microsoft.EntityFrameworkCore; + +namespace ASIS.Shared; + +/// +/// The async versions of functions require EF Core, so it's better to +/// put functions that need those here than in +/// +public static class PaginationTools +{ + /// + /// Execute the with the to create + /// a + /// + /// + /// This executes the twice: once with ApplyPaginationOptions + /// and ToListAsync to get the and a second + /// time with CountAsync to get the + /// + /// The result data type + /// + /// The query to execute to get the and + /// + /// + /// + /// The pagination options to apply to the to get the + /// + /// + public static async Task> ToPaginatedResultsAsync( + this IQueryable query, PaginationOptions paginationOptions, + CancellationToken cancellationToken = default) + where T : class + { + var results = await query + .ApplyPaginationOptions(paginationOptions) + .ToListAsync(cancellationToken); + var count = await query.CountAsync(cancellationToken); + + return new PaginatedResults( + paginationOptions, + results, + count); + } +} +```