| | | 1 | | using System.Linq.Expressions; |
| | | 2 | | using System.Reflection; |
| | | 3 | | using Microsoft.EntityFrameworkCore; |
| | | 4 | | using Microsoft.EntityFrameworkCore.Query; |
| | | 5 | | |
| | | 6 | | namespace ArturRios.Output; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Provides helpers to paginate an <see cref="IQueryable{T}"/> either |
| | | 10 | | /// synchronously or asynchronously. These extensions return a <see cref="PaginatedOutput{T}"/>. |
| | | 11 | | /// </summary> |
| | | 12 | | public static class PaginatedOutputExtensions |
| | | 13 | | { |
| | | 14 | | /// <summary> |
| | | 15 | | /// Asynchronously paginates the query and returns a <see cref="PaginatedOutput{T}"/>. |
| | | 16 | | /// </summary> |
| | | 17 | | /// <typeparam name="T">Element type of the queryable.</typeparam> |
| | | 18 | | /// <param name="query">The source query.</param> |
| | | 19 | | /// <param name="pageNumber">1-based page number.</param> |
| | | 20 | | /// <param name="pageSize">Number of items per page.</param> |
| | | 21 | | /// <param name="orderBy">Optional ordering expression.</param> |
| | | 22 | | /// <param name="totalCount">Optional total count of items in query.</param> |
| | | 23 | | /// <param name="cancellationToken">Cancellation token.</param> |
| | | 24 | | /// <returns>A task that resolves to a populated <see cref="PaginatedOutput{T}"/>.</returns> |
| | | 25 | | public static async Task<PaginatedOutput<T>> PaginateAsync<T>( |
| | | 26 | | this IQueryable<T> query, |
| | | 27 | | int pageNumber, |
| | | 28 | | int pageSize, |
| | | 29 | | Expression<Func<T, object?>>? orderBy = null, |
| | | 30 | | int? totalCount = null, |
| | | 31 | | CancellationToken cancellationToken = default) |
| | 3 | 32 | | { |
| | 3 | 33 | | pageNumber = Math.Max(1, pageNumber); |
| | 3 | 34 | | pageSize = Math.Max(1, pageSize); |
| | | 35 | | |
| | 3 | 36 | | if (orderBy != null) |
| | 1 | 37 | | { |
| | 1 | 38 | | query = query.OrderBy(orderBy); |
| | 1 | 39 | | } |
| | | 40 | | |
| | 3 | 41 | | totalCount ??= query.Provider is IAsyncQueryProvider |
| | 3 | 42 | | ? await query.CountAsync(cancellationToken).ConfigureAwait(false) |
| | 3 | 43 | | : query.Count(); |
| | | 44 | | |
| | 3 | 45 | | var skip = (pageNumber - 1) * pageSize; |
| | 3 | 46 | | var pageQuery = query.Skip(skip).Take(pageSize); |
| | | 47 | | |
| | | 48 | | List<T> items; |
| | | 49 | | |
| | 3 | 50 | | if (pageQuery.Provider is IAsyncQueryProvider) |
| | 0 | 51 | | { |
| | 0 | 52 | | items = await pageQuery.ToListAsync(cancellationToken).ConfigureAwait(false); |
| | 0 | 53 | | } |
| | | 54 | | else |
| | 3 | 55 | | { |
| | 3 | 56 | | items = pageQuery.ToList(); |
| | 3 | 57 | | } |
| | | 58 | | |
| | 3 | 59 | | return PaginatedOutput<T>.New |
| | 3 | 60 | | .WithData(items) |
| | 3 | 61 | | .WithPagination(pageNumber, totalCount.Value); |
| | 3 | 62 | | } |
| | | 63 | | |
| | | 64 | | /// <summary> |
| | | 65 | | /// Synchronously paginates the query and returns a <see cref="PaginatedOutput{T}"/>. |
| | | 66 | | /// </summary> |
| | | 67 | | /// <typeparam name="T">Element type of the queryable.</typeparam> |
| | | 68 | | /// <param name="query">The source query.</param> |
| | | 69 | | /// <param name="pageNumber">1-based page number.</param> |
| | | 70 | | /// <param name="pageSize">Number of items per page.</param> |
| | | 71 | | /// <param name="orderBy">Optional ordering expression.</param> |
| | | 72 | | /// <param name="totalCount">Optional total count of items in query.</param> |
| | | 73 | | /// <returns>A populated <see cref="PaginatedOutput{T}"/>.</returns> |
| | | 74 | | public static PaginatedOutput<T> Paginate<T>( |
| | | 75 | | this IQueryable<T> query, |
| | | 76 | | int pageNumber, |
| | | 77 | | int pageSize, |
| | | 78 | | Expression<Func<T, object?>>? orderBy = null, |
| | | 79 | | int? totalCount = null) |
| | 3 | 80 | | { |
| | 3 | 81 | | pageNumber = Math.Max(1, pageNumber); |
| | 3 | 82 | | pageSize = Math.Max(1, pageSize); |
| | | 83 | | |
| | 3 | 84 | | if (orderBy is not null) |
| | 1 | 85 | | { |
| | 1 | 86 | | query = OrderByExpression(query, orderBy); |
| | 1 | 87 | | } |
| | | 88 | | |
| | 3 | 89 | | totalCount ??= query.Count(); |
| | | 90 | | |
| | 3 | 91 | | var items = totalCount == 0 |
| | 3 | 92 | | ? [] |
| | 3 | 93 | | : query |
| | 3 | 94 | | .Skip((pageNumber - 1) * pageSize) |
| | 3 | 95 | | .Take(pageSize) |
| | 3 | 96 | | .ToList(); |
| | | 97 | | |
| | 3 | 98 | | return PaginatedOutput<T>.New |
| | 3 | 99 | | .WithData(items) |
| | 3 | 100 | | .WithPagination(pageNumber, totalCount.Value); |
| | 3 | 101 | | } |
| | | 102 | | |
| | | 103 | | /// <summary> |
| | | 104 | | /// Builds an <c>OrderBy</c> call dynamically from a lambda expression when |
| | | 105 | | /// the expression's return type is <see cref="object"/> and EF Core's |
| | | 106 | | /// expression translation would otherwise lose the typed delegate. |
| | | 107 | | /// </summary> |
| | | 108 | | private static IQueryable<T> OrderByExpression<T>(IQueryable<T> source, LambdaExpression keySelector) |
| | 1 | 109 | | { |
| | 1 | 110 | | var body = keySelector.Body; |
| | | 111 | | |
| | 1 | 112 | | if (body is UnaryExpression unary && body.NodeType == ExpressionType.Convert) |
| | 1 | 113 | | { |
| | 1 | 114 | | body = unary.Operand; |
| | 1 | 115 | | } |
| | | 116 | | |
| | 1 | 117 | | var keyType = body.Type; |
| | 1 | 118 | | var parameter = keySelector.Parameters[0]; |
| | | 119 | | |
| | 1 | 120 | | var delegateType = typeof(Func<,>).MakeGenericType(typeof(T), keyType); |
| | 1 | 121 | | var typedLambda = Expression.Lambda(delegateType, body, parameter); |
| | | 122 | | |
| | 1 | 123 | | var orderByMethod = typeof(Queryable) |
| | 1 | 124 | | .GetMethods(BindingFlags.Public | BindingFlags.Static) |
| | 21 | 125 | | .First(m => m.Name == "OrderBy" && m.GetParameters().Length == 2) |
| | 1 | 126 | | .MakeGenericMethod(typeof(T), keyType); |
| | | 127 | | |
| | 1 | 128 | | return (IQueryable<T>)orderByMethod.Invoke(null, [source, typedLambda])!; |
| | 1 | 129 | | } |
| | | 130 | | } |