Miccore.Net.Pagination
2.0.0
dotnet add package Miccore.Net.Pagination --version 2.0.0
NuGet\Install-Package Miccore.Net.Pagination -Version 2.0.0
<PackageReference Include="Miccore.Net.Pagination" Version="2.0.0" />
<PackageVersion Include="Miccore.Net.Pagination" Version="2.0.0" />
<PackageReference Include="Miccore.Net.Pagination" />
paket add Miccore.Net.Pagination --version 2.0.0
#r "nuget: Miccore.Net.Pagination, 2.0.0"
#:package Miccore.Net.Pagination@2.0.0
#addin nuget:?package=Miccore.Net.Pagination&version=2.0.0
#tool nuget:?package=Miccore.Net.Pagination&version=2.0.0
Miccore .Net Pagination
Server-side pagination library for .NET with Entity Framework Core support.
Requires .NET 8.0 or .NET 10.0
Features
- ✅ Async and sync pagination methods
- ✅ Dynamic sorting by property name
- ✅ IAsyncEnumerable streaming support
- ✅ Automatic navigation links (prev/next)
- ✅ Full nullable reference types support
- ✅ Multi-targeting .NET 8.0 and .NET 10.0
Installation
.NET CLI
dotnet add package Miccore.Net.Pagination
Package Manager
Install-Package Miccore.Net.Pagination
⚠️ Migration Guide v1 → v2
Version 2.0 introduces breaking changes. Follow this guide to migrate from v1.x:
Breaking Changes
| v1.x | v2.x | Action Required |
|---|---|---|
Miccore.Pagination.Model |
Miccore.Pagination |
Update namespace imports |
Miccore.Pagination.Service |
Miccore.Pagination |
Update namespace imports |
RouterLink class |
RouteLink class |
Rename class usage |
query.paginate |
query.Paginate |
Update property names (PascalCase) |
query.page |
query.Page |
Update property names (PascalCase) |
query.limit |
query.Limit |
Update property names (PascalCase) |
| .NET 6.0 | .NET 8.0 / 10.0 | Upgrade target framework |
Migration Steps
- Update your target framework in your
.csproj:
<TargetFramework>net8.0</TargetFramework>
- Update namespace imports:
// Before (v1)
using Miccore.Pagination.Models;
using Miccore.Pagination.Service;
// After (v2)
using Miccore.Pagination;
- Update PaginationQuery properties to PascalCase:
// Before (v1)
var query = new PaginationQuery { paginate = true, page = 1, limit = 10 };
// After (v2)
var query = new PaginationQuery { Paginate = true, Page = 1, Limit = 10 };
- Rename RouterLink to RouteLink (if used directly):
// Before (v1)
RouterLink.AddRouteLink(...)
// After (v2)
RouteLink.AddRouteLink(...)
// Or use as extension method:
response.AddRouteLink(url, query);
🔄 Deterministic Sorting
Important: When using pagination with sorting, ensure your sort order is deterministic to avoid duplicated or missing items across pages.
The Problem
If you sort by a non-unique field (e.g., CreatedAt), multiple items may have the same value. The database may return these items in an inconsistent order, causing:
- Items appearing on multiple pages
- Items being skipped entirely
The Solution
Always add a secondary sort on a unique field (like Id):
// ❌ Non-deterministic - may cause issues
var result = await _context.Products
.ApplySort("CreatedAt", "desc")
.PaginateAsync(query);
// ✅ Deterministic - safe for pagination
var result = await _context.Products
.ApplySort("CreatedAt", "desc")
.ThenBy(x => x.Id) // Secondary sort ensures uniqueness
.PaginateAsync(query);
Quick Start
Basic Usage
using Miccore.Pagination;
using Microsoft.Extensions.DependencyInjection;
// In your repository or service
public async Task<PaginationModel<Product>> GetProductsAsync(PaginationQuery query)
{
return await _context.Products.PaginateAsync(query);
}
// In your controller
[HttpGet]
public async Task<ActionResult<PaginationModel<ProductDto>>> GetProducts([FromQuery] PaginationQuery query)
{
var products = await _productService.GetProductsAsync(query);
var response = _mapper.Map<PaginationModel<ProductDto>>(products);
if (query.Paginate)
{
response.AddRouteLink(Url.RouteUrl(nameof(GetProducts)), query);
}
return Ok(response);
}
With Sorting
// Request: GET /api/products?paginate=true&page=1&limit=10&orderBy=Price&orderDirection=desc
[HttpGet]
public async Task<ActionResult<PaginationModel<ProductDto>>> GetProducts([FromQuery] PaginationQuery query)
{
// Sorting is automatically applied based on query parameters
var products = await _context.Products
.PaginateAsync(query);
// Response includes sorting info
// {
// "currentPage": 1,
// "totalPages": 5,
// "sortedBy": "Price",
// "sortDirection": "desc",
// "items": [...]
// }
return Ok(products);
}
Synchronous Pagination
// For scenarios where async is not suitable
var result = _context.Products.Paginate(query);
Streaming with IAsyncEnumerable
// Stream all items (no pagination)
await foreach (var product in _context.Products.AsStreamAsync("Name", "asc"))
{
await ProcessProduct(product);
}
// Stream paginated items
await foreach (var product in _context.Products.PaginateAsStreamAsync(query))
{
await ProcessProduct(product);
}
API Reference
PaginationQuery
| Property | Type | Default | Description |
|---|---|---|---|
Paginate |
bool |
false |
Enable/disable pagination |
Page |
int |
1 |
Page number (1-indexed) |
Limit |
int |
10 |
Items per page |
OrderBy |
string? |
null |
Property name to sort by |
OrderDirection |
string |
"asc" |
Sort direction ("asc" or "desc") |
PaginationModel<T>
| Property | Type | Description |
|---|---|---|
PageSize |
int |
Items per page (max 100) |
CurrentPage |
int |
Current page number |
TotalItems |
int |
Total item count |
TotalPages |
int |
Total page count |
Items |
List<T> |
Items for current page |
Prev |
string? |
Previous page URL |
Next |
string? |
Next page URL |
SortedBy |
string? |
Applied sort property |
SortDirection |
string? |
Applied sort direction |
Extension Methods
| Method | Description |
|---|---|
PaginateAsync<T>() |
Async pagination with sorting |
Paginate<T>() |
Sync pagination with sorting |
ApplySort<T>() |
Apply dynamic sorting |
AsStreamAsync<T>() |
Stream all items |
PaginateAsStreamAsync<T>() |
Stream paginated items |
AddRouteLink<T>() |
Add navigation URLs |
Complete Example
Repository
using Miccore.Pagination;
using Microsoft.Extensions.DependencyInjection;
public class NotificationRepository : INotificationRepository
{
private readonly AppDbContext _context;
public async Task<PaginationModel<Notification>> GetAllAsync(
PaginationQuery query,
CancellationToken cancellationToken = default)
{
return await _context.Notifications
.Where(n => !n.IsDeleted)
.PaginateAsync(query, cancellationToken);
}
}
Service
using Miccore.Pagination;
public class NotificationService : INotificationService
{
private readonly INotificationRepository _repository;
private readonly IMapper _mapper;
public async Task<PaginationModel<NotificationDto>> GetAllAsync(
PaginationQuery query,
CancellationToken cancellationToken = default)
{
var notifications = await _repository.GetAllAsync(query, cancellationToken);
return _mapper.Map<PaginationModel<NotificationDto>>(notifications);
}
}
Controller
using Miccore.Pagination;
[ApiController]
[Route("api/[controller]")]
public class NotificationsController : ControllerBase
{
private readonly INotificationService _service;
private readonly IMapper _mapper;
[HttpGet(Name = nameof(GetNotifications))]
public async Task<ActionResult<PaginationModel<NotificationViewModel>>> GetNotifications(
[FromQuery] PaginationQuery query,
CancellationToken cancellationToken)
{
var notifications = await _service.GetAllAsync(query, cancellationToken);
var response = _mapper.Map<PaginationModel<NotificationViewModel>>(notifications);
if (!query.Paginate)
{
return Ok(response.Items);
}
response.AddRouteLink(Url.RouteUrl(nameof(GetNotifications))!, query);
return Ok(response);
}
}
AutoMapper Profile
using Miccore.Pagination;
public class NotificationProfile : Profile
{
public NotificationProfile()
{
CreateMap<Notification, NotificationDto>().ReverseMap();
CreateMap<NotificationDto, NotificationViewModel>().ReverseMap();
CreateMap<PaginationModel<Notification>, PaginationModel<NotificationDto>>().ReverseMap();
CreateMap<PaginationModel<NotificationDto>, PaginationModel<NotificationViewModel>>().ReverseMap();
}
}
License
MIT License - see LICENSE for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.EntityFrameworkCore (>= 10.0.0)
-
net8.0
- Microsoft.EntityFrameworkCore (>= 8.0.11)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
v2.0.0 - Breaking Changes:
- Multi-targeting .NET 8.0 and .NET 10.0 (dropped .NET 6.0 support)
- Namespace unified to Miccore.Pagination
- PaginationQuery properties now PascalCase (paginate→Paginate, page→Page, limit→Limit)
- RouterLink class renamed to RouteLink
- Added sorting support (OrderBy, OrderDirection)
- Added synchronous Paginate() method
- Added IAsyncEnumerable streaming methods (AsStreamAsync, PaginateAsStreamAsync)
- Full nullable reference types support