< Summary

Information
Class: ArturRios.Output.PaginatedOutputExtensions
Assembly: ArturRios.Output
File(s): D:\Repositories\dotnet-output\src\PaginatedOutputExtensions.cs
Line coverage
94%
Covered lines: 54
Uncovered lines: 3
Coverable lines: 57
Total lines: 130
Line coverage: 94.7%
Branch coverage
77%
Covered branches: 17
Total branches: 22
Branch coverage: 77.2%
Method coverage
100%
Covered methods: 3
Fully covered methods: 0
Total methods: 3
Method coverage: 100%
Full method coverage: 0%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
PaginateAsync()62.5%8886.95%
Paginate(...)83.33%66100%
OrderByExpression(...)87.5%88100%

File(s)

D:\Repositories\dotnet-output\src\PaginatedOutputExtensions.cs

#LineLine coverage
 1using System.Linq.Expressions;
 2using System.Reflection;
 3using Microsoft.EntityFrameworkCore;
 4using Microsoft.EntityFrameworkCore.Query;
 5
 6namespace 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>
 12public 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)
 332    {
 333        pageNumber = Math.Max(1, pageNumber);
 334        pageSize = Math.Max(1, pageSize);
 35
 336        if (orderBy != null)
 137        {
 138            query = query.OrderBy(orderBy);
 139        }
 40
 341        totalCount ??= query.Provider is IAsyncQueryProvider
 342            ? await query.CountAsync(cancellationToken).ConfigureAwait(false)
 343            : query.Count();
 44
 345        var skip = (pageNumber - 1) * pageSize;
 346        var pageQuery = query.Skip(skip).Take(pageSize);
 47
 48        List<T> items;
 49
 350        if (pageQuery.Provider is IAsyncQueryProvider)
 051        {
 052            items = await pageQuery.ToListAsync(cancellationToken).ConfigureAwait(false);
 053        }
 54        else
 355        {
 356            items = pageQuery.ToList();
 357        }
 58
 359        return PaginatedOutput<T>.New
 360            .WithData(items)
 361            .WithPagination(pageNumber, totalCount.Value);
 362    }
 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)
 380    {
 381        pageNumber = Math.Max(1, pageNumber);
 382        pageSize = Math.Max(1, pageSize);
 83
 384        if (orderBy is not null)
 185        {
 186            query = OrderByExpression(query, orderBy);
 187        }
 88
 389        totalCount ??= query.Count();
 90
 391        var items = totalCount == 0
 392            ? []
 393            : query
 394                .Skip((pageNumber - 1) * pageSize)
 395                .Take(pageSize)
 396                .ToList();
 97
 398        return PaginatedOutput<T>.New
 399            .WithData(items)
 3100            .WithPagination(pageNumber, totalCount.Value);
 3101    }
 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)
 1109    {
 1110        var body = keySelector.Body;
 111
 1112        if (body is UnaryExpression unary && body.NodeType == ExpressionType.Convert)
 1113        {
 1114            body = unary.Operand;
 1115        }
 116
 1117        var keyType = body.Type;
 1118        var parameter = keySelector.Parameters[0];
 119
 1120        var delegateType = typeof(Func<,>).MakeGenericType(typeof(T), keyType);
 1121        var typedLambda = Expression.Lambda(delegateType, body, parameter);
 122
 1123        var orderByMethod = typeof(Queryable)
 1124            .GetMethods(BindingFlags.Public | BindingFlags.Static)
 21125            .First(m => m.Name == "OrderBy" && m.GetParameters().Length == 2)
 1126            .MakeGenericMethod(typeof(T), keyType);
 127
 1128        return (IQueryable<T>)orderByMethod.Invoke(null, [source, typedLambda])!;
 1129    }
 130}