Install
dotnet add package C0deGeek.Pagination --version 1.0.0
| Product | Versions |
|---|---|
| .NET |
README
C0deGeek.Pagination
A comprehensive, production-ready pagination solution for ASP.NET Core applications with Entity Framework Core. Features include caching, rate limiting, concurrency control, and search capabilities.
Features
- ✨ Entity Framework Core integration
- 🚀 Optimized performance with caching
- 🔒 Concurrency control with RowVersion
- 🔍 Flexible search capabilities
- 🌡️ Rate limiting (both service and API level)
- 💪 Circuit breaker and retry policies
- 🔄 HATEOAS support
- 🗜️ Response compression
- 📦 ETag support for caching
- 🛡️ Thread-safe operations
Installation
dotnet add package C0deGeek.Pagination
Quick Start
- Define your entity:
public class User : IEntity
{
public int Id { get; set; }
public required string? Name { get; set; }
public required string? Email { get; set; }
public required byte[] RowVersion { get; set; }
public DateTime LastModified { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsActive { get; set; }
}
- Set up your DbContext:
public class YourDbContext : PaginationDbContext<YourDbContext>
{
public DbSet<User> Users => Set<User>();
public YourDbContext(DbContextOptions<YourDbContext> options)
: base(options)
{
}
}
- Register services:
services.AddPagination<YourDbContext>(options =>
{
options.EnableCaching = true;
options.CacheSlidingExpiration = TimeSpan.FromMinutes(5);
options.EnableRateLimiting = true;
options.RateLimitPermitLimit = 100;
});
- Create your controller:
[ApiController]
[Route("api/[controller]")]
public class UsersController : PaginationControllerBase<User>
{
private readonly PaginationService<User, YourDbContext> _paginationService;
public UsersController(PaginationService<User, YourDbContext> paginationService)
{
_paginationService = paginationService;
}
[HttpGet]
[EnableRateLimiting("fixed")]
public async Task<IActionResult> GetUsers(
[FromQuery] PaginationParameters parameters,
[FromHeader(Name = "If-None-Match")] string? ifNoneMatch = null,
CancellationToken cancellationToken = default)
{
try
{
var result = await _paginationService.GetPagedDataAsync(
parameters,
cancellationToken);
if (result is null)
{
return StatusCode(304); // Not Modified
}
SetPaginationHeaders(result);
return Ok(new PaginationResponse<User>(
result.Items,
result,
GenerateLinks(result, nameof(GetUsers), new { parameters.PageSize })
));
}
catch (RateLimitExceededException)
{
return StatusCode(429, "Too many requests");
}
}
}
Configuration Options
Pagination Options
services.AddPagination<YourDbContext>(options =>
{
// Caching
options.EnableCaching = true;
options.CacheSlidingExpiration = TimeSpan.FromMinutes(5);
options.CacheAbsoluteExpiration = TimeSpan.FromHours(1);
// Rate Limiting
options.EnableRateLimiting = true;
options.RateLimitPermitLimit = 100;
options.RateLimitWindowSeconds = 1;
// Circuit Breaker
options.CircuitBreakerFailureThreshold = 5;
options.CircuitBreakerSamplingDuration = TimeSpan.FromSeconds(30);
options.CircuitBreakerDurationOfBreak = TimeSpan.FromSeconds(60);
// Retry Policy
options.RetryCount = 3;
options.RetryBaseDelayMs = 100;
// Database
options.IsolationLevel = IsolationLevel.ReadCommitted;
});
Rate Limiting Options
Choose between service-level, API-level, or both:
// Service-level only
services.AddPaginationWithServiceRateLimit<YourDbContext>(options =>
{
options.EnableRateLimiting = true;
options.RateLimitPermitLimit = 100;
});
// API-level only
services.AddPaginationWithApiRateLimit<YourDbContext>(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromSeconds(1);
});
});
Search Capabilities
Option 1: Implement ISearchableEntity
public class User : IEntity, ISearchableEntity
{
// ... other properties
public bool MatchesSearchTerm(string searchTerm)
{
return (Name?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ?? false) ||
(Email?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ?? false);
}
}
Option 2: Create a Search Provider
public class UserSearchProvider : ISearchExpressionProvider<User>
{
public Expression<Func<User, bool>> GetSearchExpression(string searchTerm)
{
return user =>
EF.Functions.Like(user.Name, $"%{searchTerm}%") ||
EF.Functions.Like(user.Email, $"%{searchTerm}%");
}
}
// Register the provider
services.AddSearchProvider<User, UserSearchProvider>();
Response Format
The API returns responses in this format:
{
"data": [
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"isActive": true
}
],
"pagination": {
"currentPage": 1,
"pageSize": 10,
"totalPages": 5,
"totalItems": 48,
"hasNextPage": true,
"hasPreviousPage": false,
"links": [
{
"href": "/api/users?pageNumber=1&pageSize=10",
"rel": "self",
"method": "GET"
},
{
"href": "/api/users?pageNumber=2&pageSize=10",
"rel": "next",
"method": "GET"
}
]
}
}
Response Headers
ETag: For cache validationLast-Modified: Last modification timestampX-Total-Count: Total number of itemsX-Total-Pages: Total number of pages
Error Handling
The package provides these status codes:
200 OK: Successful request304 Not Modified: Content hasn't changed429 Too Many Requests: Rate limit exceeded503 Service Unavailable: Circuit breaker open
Advanced Features
Custom Cache Key Generation
public class CustomCacheKeyProvider : ICacheKeyProvider<User>
{
public string GetCacheKey(PaginationParameters parameters)
{
return $"custom_key_{parameters.GetHashCode()}";
}
}
services.AddScoped<ICacheKeyProvider<User>, CustomCacheKeyProvider>();
Custom Sort Expressions
public class UserSortProvider : ISortExpressionProvider<User>
{
public IQueryable<User> ApplySort(
IQueryable<User> query,
string sortBy,
bool descending)
{
// Custom sorting logic
}
}
services.AddScoped<ISortExpressionProvider<User>, UserSortProvider>();
Best Practices
- Always use cancellation tokens for async operations
- Implement proper error handling
- Set appropriate cache durations
- Configure rate limits based on your API's capacity
- Monitor circuit breaker events
- Use appropriate isolation levels for your use case
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE.md file for details
Acknowledgments
- Built with ASP.NET Core and Entity Framework Core
- Uses Polly for resilience patterns
- Inspired by REST best practices and HATEOAS principles
Adoption Assessment
Assess this package against its supported frameworks, license, maintenance signals and dependency graph before making it a project standard.
No SPDX license expression is published. Review the license text and your organisation's policy before redistribution.Compatibility metadata lists 1 target framework; confirm the exact target used by your application.
Related Guides & Workflows
Learn how to evaluate, integrate, or audit this package in your .NET project.