Add post about C# pagination
This commit is contained in:
		
							parent
							
								
									5bb6f043d9
								
							
						
					
					
						commit
						69b423b565
					
				
							
								
								
									
										329
									
								
								src/posts/CsPaginationTools.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										329
									
								
								src/posts/CsPaginationTools.md
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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<Thing> 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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// <summary>Defines how to get paginated data</summary>
 | 
				
			||||||
 | 
					[DataContract(IsReference = true)]
 | 
				
			||||||
 | 
					public class PaginationOptions
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// This constructor exists so that MVC can instantiate the object before mapping is contents
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						public PaginationOptions() { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public PaginationOptions(int pageNumber, int? resultsPerPage)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							this.PageNumber = pageNumber;
 | 
				
			||||||
 | 
							this.ResultsPerPage = resultsPerPage;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private int? _resultsPerPage;
 | 
				
			||||||
 | 
						private int _pageNumber;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>The number of results per page</summary>
 | 
				
			||||||
 | 
						[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;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// The page number to get
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						/// <remarks>One indexed</remarks>
 | 
				
			||||||
 | 
						[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;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// <summary>
 | 
				
			||||||
 | 
					/// An extension of <see cref="PaginationOptions"/> that contains the results from a query and uses
 | 
				
			||||||
 | 
					/// them to calculate additional pagination info
 | 
				
			||||||
 | 
					/// </summary>
 | 
				
			||||||
 | 
					/// <typeparam name="T">
 | 
				
			||||||
 | 
					/// The <see cref="IEnumerable{U}"/> type that will contain the data (<typeparamref name="U"/>)
 | 
				
			||||||
 | 
					/// </typeparam>
 | 
				
			||||||
 | 
					/// <typeparam name="U">The data type of the results</typeparam>
 | 
				
			||||||
 | 
					[DataContract(IsReference = true)]
 | 
				
			||||||
 | 
					public class PaginatedResults<T> : PaginationOptions where T : class
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						[JsonConstructor]
 | 
				
			||||||
 | 
						public PaginatedResults(int pageNumber, int? resultsPerPage, IEnumerable<T> results,
 | 
				
			||||||
 | 
							int totalResultsCount) : base(pageNumber, resultsPerPage)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							this.Results = results;
 | 
				
			||||||
 | 
							this.TotalResultsCount = totalResultsCount;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public PaginatedResults(PaginationOptions? paginationOptions, IEnumerable<T> results,
 | 
				
			||||||
 | 
							int totalResultsCount)
 | 
				
			||||||
 | 
							: base(paginationOptions?.PageNumber ?? 1, paginationOptions?.ResultsPerPage)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							this.Results = results;
 | 
				
			||||||
 | 
							this.TotalResultsCount = totalResultsCount;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// The total number of results being paginated. This is used for determining where you are
 | 
				
			||||||
 | 
						/// in the list.
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						[DataMember]
 | 
				
			||||||
 | 
						public int TotalResultsCount { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// The number of results on the current page
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						[DataMember]
 | 
				
			||||||
 | 
						public int? CurrentPageResultsCount { get => this.Results.Count(); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// The number of the first result on the current page. This is calculated using
 | 
				
			||||||
 | 
						/// <see cref="ResultsPerPage"/>, <see cref="PageNumber"/>, and <see cref="TotalResults"/>.
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						[DataMember]
 | 
				
			||||||
 | 
						public int? FirstResultNumber
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							get
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								if (this.ResultsPerPage is null)
 | 
				
			||||||
 | 
									return 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return (this.ResultsPerPage * (this.PageNumber - 1)) + 1;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// The number of the last result on the current page. This is calculated using
 | 
				
			||||||
 | 
						/// <see cref="ResultsPerPage"/>, <see cref="PageNumber"/>, and <see cref="TotalResults"/>.
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						[DataMember]
 | 
				
			||||||
 | 
						public int? LastResultNumber
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							get
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								if (this.ResultsPerPage is null)
 | 
				
			||||||
 | 
									return this.Results.Count();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return (this.ResultsPerPage * (this.PageNumber - 1)) + this.CurrentPageResultsCount;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// The total number of pages based on the <see cref="ResultsPerPage"/> and
 | 
				
			||||||
 | 
						/// <see cref="TotalResultsCount"/>
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						[DataMember]
 | 
				
			||||||
 | 
						public int? LastPageNumber
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							get
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								if (this.ResultsPerPage is null)
 | 
				
			||||||
 | 
									return 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return (int)Math.Ceiling(this.TotalResultsCount
 | 
				
			||||||
 | 
									/ (double)this.ResultsPerPage);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// A shortcut that checks if <see cref="PaginationOptions.PageNumber"/> is equal to 1
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						[DataMember]
 | 
				
			||||||
 | 
						public bool IsFirstPage { get => this.PageNumber == 1; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// A shortcut that checks if <see cref="PaginationOptions.PageNumber"/> is equal to
 | 
				
			||||||
 | 
						/// <see cref="LastPageNumber"/>
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						[DataMember]
 | 
				
			||||||
 | 
						public bool IsLastPage { get => this.PageNumber == this.LastPageNumber; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>A shortcut to check if the current page is not <c>1</c></summary>
 | 
				
			||||||
 | 
						[DataMember]
 | 
				
			||||||
 | 
						public bool HasPreviousPage { get => !this.IsFirstPage; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// A shortcut to check if the current page is not <see cref="LastPageNumber"/>
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						[DataMember]
 | 
				
			||||||
 | 
						public bool HasNextPage { get => !this.IsLastPage; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// The number of the previous page or <c>1</c> if <see cref="IsFirstPage"/>
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						[DataMember]
 | 
				
			||||||
 | 
						public int PreviousPageNumber
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							get
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								if (this.IsFirstPage)
 | 
				
			||||||
 | 
									return 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return this.PageNumber - 1;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// The number of the next page or <see cref="LastPageNumber"/> if <see cref="IsLastPage"/>
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						[DataMember]
 | 
				
			||||||
 | 
						public int NextPageNumber
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							get
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								if (this.IsLastPage)
 | 
				
			||||||
 | 
									return this.LastPageNumber;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return this.PageNumber + 1;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[DataMember]
 | 
				
			||||||
 | 
						public IEnumerable<T> Results { get; set; }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public static class PaginationOptionsExtensionMethods
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// Applies a Skip/Take to the query and returns it
 | 
				
			||||||
 | 
						/// <para>
 | 
				
			||||||
 | 
						/// Simply returns the unmodified <paramref name="query"/> if the <paramref name="options"/> is
 | 
				
			||||||
 | 
						/// null (i.e. null means don't paginate)
 | 
				
			||||||
 | 
						/// </para>
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						/// <param name="query">The query to paginate</param>
 | 
				
			||||||
 | 
						/// <param name="options">The options for paginating the query</param>
 | 
				
			||||||
 | 
						public static IQueryable<T> ApplyPaginationOptions<T>(this IQueryable<T> 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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// Execute the <paramref name="query"/> with the <paramref name="paginationOptions"/> to create
 | 
				
			||||||
 | 
						/// a <see cref="PaginatedResults{T, U}"/>
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						/// <remarks>
 | 
				
			||||||
 | 
						/// This executes the <paramref name="query"/> twice: once with <c>ApplyPaginationOptions</c>
 | 
				
			||||||
 | 
						/// and <c>ToList</c> to get the <see cref="PaginatedResults{T, U}.Results"/> and a second
 | 
				
			||||||
 | 
						/// time with <c>Count</c> to get the <see cref="PaginatedResults{T, U}.TotalResultsCount"/>
 | 
				
			||||||
 | 
						/// </remarks>
 | 
				
			||||||
 | 
						/// <typeparam name="T">The result data type</typeparam>
 | 
				
			||||||
 | 
						/// <param name="query">
 | 
				
			||||||
 | 
						/// The query to execute to get the <see cref="PaginatedResults{T, U}.Results"/> and
 | 
				
			||||||
 | 
						/// <see cref="PaginatedResults{T, U}.TotalResultsCount"/>
 | 
				
			||||||
 | 
						/// </param>
 | 
				
			||||||
 | 
						/// <param name="paginationOptions">
 | 
				
			||||||
 | 
						/// The pagination options to apply to the <paramref name="query"/> to get the
 | 
				
			||||||
 | 
						/// <see cref="PaginatedResults{T, U}.Results"/>
 | 
				
			||||||
 | 
						/// </param>
 | 
				
			||||||
 | 
						public static PaginatedResults<T> ToPaginatedResults<T>(this IQueryable<T> query,
 | 
				
			||||||
 | 
							PaginationOptions? paginationOptions)
 | 
				
			||||||
 | 
							where T : class
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							return new PaginatedResults<T>(
 | 
				
			||||||
 | 
								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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// <summary>
 | 
				
			||||||
 | 
					/// The async versions of <see cref="IQueryable{T}"/> functions require EF Core, so it's better to
 | 
				
			||||||
 | 
					/// put functions that need those here than in <see cref="UREC.Attic.Shared"/>
 | 
				
			||||||
 | 
					/// </summary>
 | 
				
			||||||
 | 
					public static class PaginationTools
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						/// <summary>
 | 
				
			||||||
 | 
						/// Execute the <paramref name="query"/> with the <paramref name="paginationOptions"/> to create
 | 
				
			||||||
 | 
						/// a <see cref="PaginatedResults{T}"/>
 | 
				
			||||||
 | 
						/// </summary>
 | 
				
			||||||
 | 
						/// <remarks>
 | 
				
			||||||
 | 
						/// This executes the <paramref name="query"/> twice: once with <c>ApplyPaginationOptions</c>
 | 
				
			||||||
 | 
						/// and <c>ToListAsync</c> to get the <see cref="PaginatedResults{T}.Results"/> and a second
 | 
				
			||||||
 | 
						/// time with <c>CountAsync</c> to get the <see cref="PaginatedResults{T}.TotalResultsCount"/>
 | 
				
			||||||
 | 
						/// </remarks>
 | 
				
			||||||
 | 
						/// <typeparam name="T">The result data type</typeparam>
 | 
				
			||||||
 | 
						/// <param name="query">
 | 
				
			||||||
 | 
						/// The query to execute to get the <see cref="PaginatedResults{T}.Results"/> and
 | 
				
			||||||
 | 
						/// <see cref="PaginatedResults{T}.TotalResultsCount"/>
 | 
				
			||||||
 | 
						/// </param>
 | 
				
			||||||
 | 
						/// <param name="paginationOptions">
 | 
				
			||||||
 | 
						/// The pagination options to apply to the <paramref name="query"/> to get the
 | 
				
			||||||
 | 
						/// <see cref="PaginatedResults{T}.Results"/>
 | 
				
			||||||
 | 
						/// </param>
 | 
				
			||||||
 | 
						public static async Task<PaginatedResults<T>> ToPaginatedResultsAsync<T>(
 | 
				
			||||||
 | 
							this IQueryable<T> 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<T>(
 | 
				
			||||||
 | 
								paginationOptions,
 | 
				
			||||||
 | 
								results,
 | 
				
			||||||
 | 
								count);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
		Loading…
	
		Reference in a new issue