-
Notifications
You must be signed in to change notification settings - Fork 0
Release v1.0 #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Release v1.0 #25
Conversation
Introduces HATEOAS-optimized pagination with new DTOs, link generator interface and implementation, and supporting repository and queryable extensions. Enables efficient paged API responses with navigation links, avoiding total count queries for performance. Registers PaginationLinkGenerator in DI container.
Add HATEOAS pagination support and link generator
Reviewer's GuideAdds a HATEOAS-oriented pagination infrastructure (DTOs, queryable extensions, and link generator) and wires the pagination link generator into the ASP.NET Core DI module, while doing minor formatting tweaks to existing response compression configuration. Sequence diagram for generating a HATEOAS paged API responsesequenceDiagram
actor ApiClient
participant AspNetApp
participant ResourceController
participant Repository
participant QueryableExtensions
participant IPaginationLinkGenerator
participant PaginationLinkGenerator
participant HttpContextAccessor
participant HttpContext
participant HateoasPagedResultDto
ApiClient->>AspNetApp: HTTP GET /resources?page=2&pageSize=10
AspNetApp->>ResourceController: Route request
ResourceController->>Repository: GetQueryable()
Repository-->>ResourceController: IQueryable<TEntity>
ResourceController->>QueryableExtensions: ToHateoasPagedListAsync(query, pageNumber, pageSize, cancellationToken)
activate QueryableExtensions
QueryableExtensions->>Repository: Execute query with PageBy(skip, pageSize+1)
Repository-->>QueryableExtensions: List<TEntity> items
QueryableExtensions-->>ResourceController: HateoasPagedList<TEntity>
deactivate QueryableExtensions
ResourceController->>ResourceController: Map entities to IReadOnlyList<TDto>
ResourceController->>IPaginationLinkGenerator: CreateHateoasResult(pagedList, items, routePath)
activate IPaginationLinkGenerator
IPaginationLinkGenerator->>PaginationLinkGenerator: Dispatch to implementation
PaginationLinkGenerator->>PaginationLinkGenerator: GenerateLinks(pagedList, routePath)
activate PaginationLinkGenerator
PaginationLinkGenerator->>HttpContextAccessor: HttpContext
HttpContextAccessor-->>PaginationLinkGenerator: HttpContext
PaginationLinkGenerator->>HttpContext: Read Request.Scheme, Host, PathBase, Query, X-Forwarded-* headers
HttpContext-->>PaginationLinkGenerator: Request data
PaginationLinkGenerator-->>IPaginationLinkGenerator: PaginationLinks
deactivate PaginationLinkGenerator
IPaginationLinkGenerator-->>ResourceController: HateoasPagedResultDto<TDto>
deactivate IPaginationLinkGenerator
ResourceController-->>AspNetApp: 200 OK with HateoasPagedResultDto<TDto>
AspNetApp-->>ApiClient: JSON body with items and pagination links
Class diagram for new HATEOAS pagination componentsclassDiagram
direction LR
class IPaginationLinkGenerator {
<<interface>>
+PaginationLinks GenerateLinks~T~(HateoasPagedList~T~ pagedList, string routePath)
+HateoasPagedResultDto~TDto~ CreateHateoasResult~TEntity, TDto~(HateoasPagedList~TEntity~ pagedList, IReadOnlyList~TDto~ items, string routePath)
+PaginationLinks GenerateLinks~T~(PagedList~T~ pagedList, string routePath)
+HateoasPagedResultDto~TDto~ CreateHateoasResult~TEntity, TDto~(PagedList~TEntity~ pagedList, IReadOnlyList~TDto~ items, string routePath)
}
class PaginationLinkGenerator {
-IHttpContextAccessor _httpContextAccessor
+PaginationLinkGenerator(IHttpContextAccessor httpContextAccessor)
+PaginationLinks GenerateLinks~T~(HateoasPagedList~T~ pagedList, string routePath)
+HateoasPagedResultDto~TDto~ CreateHateoasResult~TEntity, TDto~(HateoasPagedList~TEntity~ pagedList, IReadOnlyList~TDto~ items, string routePath)
+PaginationLinks GenerateLinks~T~(PagedList~T~ pagedList, string routePath)
+HateoasPagedResultDto~TDto~ CreateHateoasResult~TEntity, TDto~(PagedList~TEntity~ pagedList, IReadOnlyList~TDto~ items, string routePath)
-IQueryCollection GetCurrentQueryParams()
-string GetBaseUrl()
-static string GetForwardedScheme(HttpRequest request)
-static string GetForwardedHost(HttpRequest request)
-static string BuildPageLink(string baseUrl, string route, int page, int pageSize, IQueryCollection queryParams)
}
class HateoasPagedList~T~ {
+HateoasPagedList(IList~T~ items, int pageNumber, int pageSize, bool hasNext)
+int CurrentPage
+int PageSize
+bool HasPrevious
+bool HasNext
+IList~T~ Items
}
class PaginationLinks {
+string Self
+string First
+string Next
+string Prev
}
class HateoasPagedResultDto~T~ {
+PaginationLinks Links
+HateoasPagedResultDto()
+HateoasPagedResultDto(IReadOnlyList~T~ items, PaginationLinks links)
}
class ListResultDto~T~ {
<<existing>>
+IReadOnlyList~T~ Items
}
class PagedList~T~ {
<<existing>>
+IReadOnlyList~T~ Items
+long TotalCount
+int CurrentPage
+int PageSize
+bool HasNext
+bool HasPrevious
}
class PaginationParameters {
<<existing>>
+int SkipCount
+int MaxResultCount
}
class QueryableExtensions {
<<static>>
+Task~PagedList~T~~ ToPagedListAsync~T~(IQueryable~T~ query, int pageNumber, int pageSize, CancellationToken cancellationToken)
+Task~PagedList~T~~ ToPagedListAsync~T~(IQueryable~T~ query, PaginationParameters parameters, CancellationToken cancellationToken)
+Task~HateoasPagedList~T~~ ToHateoasPagedListAsync~T~(IQueryable~T~ query, int pageNumber, int pageSize, CancellationToken cancellationToken)
+Task~HateoasPagedList~T~~ ToHateoasPagedListAsync~T~(IQueryable~T~ query, PaginationParameters parameters, CancellationToken cancellationToken)
}
class IHttpContextAccessor {
<<framework>>
+HttpContext HttpContext
}
class HttpRequest {
<<framework>>
+string Scheme
+HostString Host
+PathString PathBase
+IHeaderDictionary Headers
+IQueryCollection Query
}
IPaginationLinkGenerator <|.. PaginationLinkGenerator
HateoasPagedResultDto~T~ --|> ListResultDto~T~
PaginationLinkGenerator --> IHttpContextAccessor
PaginationLinkGenerator --> PaginationLinks
PaginationLinkGenerator --> HateoasPagedList~T~
PaginationLinkGenerator --> PagedList~T~
QueryableExtensions --> PagedList~T~
QueryableExtensions --> HateoasPagedList~T~
QueryableExtensions --> PaginationParameters
PaginationLinkGenerator --> HttpRequest
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Caution Review failedThe pull request is closed. Note
|
| Cohort / File(s) | Summary |
|---|---|
HATEOAS DTOs framework/src/BBT.Aether.Application/BBT/Aether/Application/Dtos/HateoasPagedResultDto.cs, framework/src/BBT.Aether.Application/BBT/Aether/Application/Dtos/PaginationLinks.cs |
Added generic HateoasPagedResultDto<T> extending ListResultDto<T> with Links property and constructors. Added PaginationLinks DTO exposing Self, First, Next, and Prev navigation URLs as empty strings by default. |
Domain Models & Interfaces framework/src/BBT.Aether.Domain/BBT/Aether/Domain/Repositories/HateoasPagedList.cs, framework/src/BBT.Aether.Application/BBT/Aether/Domain/Pagination/IPaginationLinkGenerator.cs |
Added HateoasPagedList<T> model with Items, CurrentPage, PageSize, HasPrevious, and HasNext (no TotalCount). Added IPaginationLinkGenerator interface with four overloaded methods for generating links and creating HATEOAS results. |
ASP.NET Core Implementation framework/src/BBT.Aether.AspNetCore/BBT/Aether/Domain/Pagination/PaginationLinkGenerator.cs |
Implemented PaginationLinkGenerator using IHttpContextAccessor to derive base URLs with reverse proxy header support (X-Forwarded-Proto, X-Forwarded-Host). Builds Self, First, Next, Prev links while preserving non-pagination query parameters. |
Dependency Injection Registration framework/src/BBT.Aether.AspNetCore/Microsoft/Extensions/DependencyInjection/AetherAspNetCoreModuleServiceCollectionExtensions.cs |
Registered IPaginationLinkGenerator mapped to PaginationLinkGenerator via AddScoped, added using directive for BBT.Aether.Domain.Pagination. |
Query Extensions framework/src/BBT.Aether.Infrastructure/BBT/Aether/Domain/QueryableExtensions.cs |
Added four extension methods on IQueryable: ToPagedListAsync (with total count) and ToHateoasPagedListAsync (N+1 strategy for hasNext), each with overloads accepting raw pagination parameters or PaginationParameters object. |
Sequence Diagram
sequenceDiagram
participant Controller
participant QueryableExtensions as Query Extensions
participant Database as DB
participant PaginationLinkGenerator as Link Generator
participant Result as Response DTO
Controller->>QueryableExtensions: ToHateoasPagedListAsync(query, page, size)
QueryableExtensions->>Database: Fetch pageSize+1 items (N+1 strategy)
Database-->>QueryableExtensions: Items + extra for hasNext
QueryableExtensions-->>Controller: HateoasPagedList<T>
Controller->>PaginationLinkGenerator: GenerateLinks(pagedList, routePath)
Note over PaginationLinkGenerator: Extract base URL<br/>Honor X-Forwarded-* headers
PaginationLinkGenerator-->>Controller: PaginationLinks (Self, First, Next, Prev)
Controller->>PaginationLinkGenerator: CreateHateoasResult(pagedList, items, routePath)
PaginationLinkGenerator-->>Controller: HateoasPagedResultDto<T>
Controller-->>Result: Return HATEOAS response
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes
- PaginationLinkGenerator implementation: Verify reverse proxy header handling (X-Forwarded-Proto, X-Forwarded-Host fallback logic) and link construction with query parameter preservation.
- QueryableExtensions N+1 strategy: Review the paging logic for fetching pageSize+1 items to determine hasNext and confirm the extra item is properly trimmed before returning.
- Interface contracts: Validate that all four overloaded methods in
IPaginationLinkGeneratorare correctly implemented with proper generic constraints and type mapping (TEntity vs TDto). - DI registration scope: Confirm
AddScopedlifetime is appropriate forIPaginationLinkGeneratorgiven itsIHttpContextAccessordependency.
Poem
🐰 Through pages we hop with links in hand,
Self, First, Next—a HATEOAS land!
No counts to fetch, just hasNext to know,
Reverse proxies honored as we go! 🔗
✨ Finishing touches
- 📝 Generate docstrings
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
release-v1.0
📜 Recent review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
framework/src/BBT.Aether.Application/BBT/Aether/Application/Dtos/HateoasPagedResultDto.cs(1 hunks)framework/src/BBT.Aether.Application/BBT/Aether/Application/Dtos/PaginationLinks.cs(1 hunks)framework/src/BBT.Aether.Application/BBT/Aether/Domain/Pagination/IPaginationLinkGenerator.cs(1 hunks)framework/src/BBT.Aether.AspNetCore/BBT/Aether/Domain/Pagination/PaginationLinkGenerator.cs(1 hunks)framework/src/BBT.Aether.AspNetCore/Microsoft/Extensions/DependencyInjection/AetherAspNetCoreModuleServiceCollectionExtensions.cs(4 hunks)framework/src/BBT.Aether.Domain/BBT/Aether/Domain/Repositories/HateoasPagedList.cs(1 hunks)framework/src/BBT.Aether.Infrastructure/BBT/Aether/Domain/QueryableExtensions.cs(1 hunks)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
Summary of ChangesHello @yilmaztayfun, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the framework's API capabilities by integrating HATEOAS-driven pagination. It introduces a robust mechanism for generating dynamic navigation links within paginated results, offering both standard total-count-inclusive paging and a performance-optimized approach that omits total counts for scenarios where they are not strictly necessary. This change aims to improve API discoverability and efficiency for consumers. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes and they look great!
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `framework/src/BBT.Aether.Infrastructure/BBT/Aether/Domain/QueryableExtensions.cs:63-64` </location>
<code_context>
+ PaginationParameters parameters,
+ CancellationToken cancellationToken = default)
+ {
+ var pageSize = parameters.MaxResultCount;
+ var pageNumber = (parameters.SkipCount / pageSize) + 1;
+
+ return await query.ToPagedListAsync(pageNumber, pageSize, cancellationToken);
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against zero/invalid MaxResultCount to avoid divide-by-zero and inconsistent paging.
Here `pageSize` is used to compute `pageNumber` before any safety checks, so a 0 or negative `MaxResultCount` will cause issues. Align this overload with the other one by normalizing `pageSize` to a minimum value (e.g., default to 10 when `< 1`) before calculating `pageNumber` and calling `ToPagedListAsync`.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| var pageSize = parameters.MaxResultCount; | ||
| var pageNumber = (parameters.SkipCount / pageSize) + 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Guard against zero/invalid MaxResultCount to avoid divide-by-zero and inconsistent paging.
Here pageSize is used to compute pageNumber before any safety checks, so a 0 or negative MaxResultCount will cause issues. Align this overload with the other one by normalizing pageSize to a minimum value (e.g., default to 10 when < 1) before calculating pageNumber and calling ToPagedListAsync.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces HATEOAS-friendly pagination support, which is a great feature. The implementation is solid, with new DTOs, a link generator service, and IQueryable extensions. My review includes a few suggestions to improve API design, code maintainability by reducing duplication, and adherence to C# conventions. Overall, this is a well-executed feature addition.
| /// <summary> | ||
| /// Link to the current page. | ||
| /// </summary> | ||
| public string Self { get; set; } = string.Empty; | ||
|
|
||
| /// <summary> | ||
| /// Link to the first page. | ||
| /// </summary> | ||
| public string First { get; set; } = string.Empty; | ||
|
|
||
| /// <summary> | ||
| /// Link to the next page. Empty if no next page exists. | ||
| /// </summary> | ||
| public string Next { get; set; } = string.Empty; | ||
|
|
||
| /// <summary> | ||
| /// Link to the previous page. Empty if on first page. | ||
| /// </summary> | ||
| public string Prev { get; set; } = string.Empty; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The properties for links are currently non-nullable strings initialized to string.Empty. For HATEOAS links in a JSON API, it's more idiomatic to use null to represent a link that doesn't exist. This allows JSON serializers to omit the property entirely (depending on configuration), leading to a cleaner and smaller API response.
I suggest changing these properties to be nullable strings (string?) and removing the string.Empty initializers. This would also require updating PaginationLinkGenerator.cs to use null instead of string.Empty for absent Next and Prev links.
/// <summary>
/// Link to the current page.
/// </summary>
public string? Self { get; set; }
/// <summary>
/// Link to the first page.
/// </summary>
public string? First { get; set; }
/// <summary>
/// Link to the next page. Null if no next page exists.
/// </summary>
public string? Next { get; set; }
/// <summary>
/// Link to the previous page. Null if on first page.
/// </summary>
public string? Prev { get; set; }| public PaginationLinks GenerateLinks<T>(HateoasPagedList<T> pagedList, string routePath) | ||
| { | ||
| var baseUrl = GetBaseUrl(); | ||
| var route = routePath.TrimStart('/'); | ||
| var queryParams = GetCurrentQueryParams(); | ||
|
|
||
| return new PaginationLinks | ||
| { | ||
| Self = BuildPageLink(baseUrl, route, pagedList.CurrentPage, pagedList.PageSize, queryParams), | ||
| First = BuildPageLink(baseUrl, route, 1, pagedList.PageSize, queryParams), | ||
| Next = pagedList.HasNext | ||
| ? BuildPageLink(baseUrl, route, pagedList.CurrentPage + 1, pagedList.PageSize, queryParams) | ||
| : string.Empty, | ||
| Prev = pagedList.HasPrevious | ||
| ? BuildPageLink(baseUrl, route, pagedList.CurrentPage - 1, pagedList.PageSize, queryParams) | ||
| : string.Empty | ||
| }; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public HateoasPagedResultDto<TDto> CreateHateoasResult<TEntity, TDto>( | ||
| HateoasPagedList<TEntity> pagedList, | ||
| IReadOnlyList<TDto> items, | ||
| string routePath) | ||
| { | ||
| var links = GenerateLinks(pagedList, routePath); | ||
| return new HateoasPagedResultDto<TDto>(items, links); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public PaginationLinks GenerateLinks<T>(PagedList<T> pagedList, string routePath) | ||
| { | ||
| var baseUrl = GetBaseUrl(); | ||
| var route = routePath.TrimStart('/'); | ||
| var queryParams = GetCurrentQueryParams(); | ||
|
|
||
| return new PaginationLinks | ||
| { | ||
| Self = BuildPageLink(baseUrl, route, pagedList.CurrentPage, pagedList.PageSize, queryParams), | ||
| First = BuildPageLink(baseUrl, route, 1, pagedList.PageSize, queryParams), | ||
| Next = pagedList.HasNext | ||
| ? BuildPageLink(baseUrl, route, pagedList.CurrentPage + 1, pagedList.PageSize, queryParams) | ||
| : string.Empty, | ||
| Prev = pagedList.HasPrevious | ||
| ? BuildPageLink(baseUrl, route, pagedList.CurrentPage - 1, pagedList.PageSize, queryParams) | ||
| : string.Empty | ||
| }; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public HateoasPagedResultDto<TDto> CreateHateoasResult<TEntity, TDto>( | ||
| PagedList<TEntity> pagedList, | ||
| IReadOnlyList<TDto> items, | ||
| string routePath) | ||
| { | ||
| var links = GenerateLinks(pagedList, routePath); | ||
| return new HateoasPagedResultDto<TDto>(items, links); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GenerateLinks methods for HateoasPagedList<T> and PagedList<T> contain identical logic. Similarly, the CreateHateoasResult methods are also duplicates. This code duplication can make future maintenance more difficult and violates the DRY (Don't Repeat Yourself) principle.
I recommend refactoring to remove this duplication. You could extract the common logic into a private helper method. Since both HateoasPagedList<T> and PagedList<T> have the required pagination properties (CurrentPage, PageSize, HasNext, HasPrevious), you could introduce a common interface that both types implement, and have the helper method accept that interface. This would make the code more maintainable.
| /// <param name="pageSize">The number of items per page.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>A PagedList containing the paginated results with total count.</returns> | ||
| public async static Task<PagedList<T>> ToPagedListAsync<T>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The C# convention for async methods is to place the async keyword after other modifiers like static. Using public static async is preferred over public async static for consistency and readability. This issue is present for all async methods in this file (lines 24, 58, 79, 122).
public static async Task<PagedList<T>> ToPagedListAsync<T>(| /// <param name="parameters">The pagination parameters.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>A PagedList containing the paginated results with total count.</returns> | ||
| public async static Task<PagedList<T>> ToPagedListAsync<T>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// <param name="pageSize">The number of items per page.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>A HateoasPagedList containing the paginated results (no TotalCount for performance).</returns> | ||
| public async static Task<HateoasPagedList<T>> ToHateoasPagedListAsync<T>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// <param name="parameters">The pagination parameters.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>A HateoasPagedList containing the paginated results (no TotalCount for performance).</returns> | ||
| public async static Task<HateoasPagedList<T>> ToHateoasPagedListAsync<T>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
|
||
| return await query.ToHateoasPagedListAsync(pageNumber, pageSize, cancellationToken); | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|



Summary by Sourcery
Introduce framework-level support for HATEOAS-friendly pagination in ASP.NET Core APIs and wire it into the Aether ASP.NET Core module.
New Features:
Enhancements:
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.