Skip to content

Conversation

@yilmaztayfun
Copy link
Contributor

@yilmaztayfun yilmaztayfun commented Dec 1, 2025

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:

  • Add a PaginationLinkGenerator service and IPaginationLinkGenerator interface to generate HATEOAS pagination links based on the current HTTP request, including reverse proxy support.
  • Introduce HateoasPagedList, HateoasPagedResultDto, and PaginationLinks types to represent paginated data and navigation links in API responses.
  • Provide IQueryable extension methods to materialize queries into PagedList and HateoasPagedList instances, including overloads that accept PaginationParameters.

Enhancements:

  • Register the PaginationLinkGenerator as a scoped service in the Aether ASP.NET Core module configuration.
  • Tidy minor formatting in the ASP.NET Core response compression and exception handler configuration code.

Summary by CodeRabbit

  • New Features
    • Added HATEOAS pagination support for paginated API responses with self, first, next, and previous page navigation links.
    • Introduced optimized pagination handling that eliminates total count queries for improved performance.
    • Automatically generates pagination links with support for reverse proxy headers.

✏️ Tip: You can customize this high-level summary in your review settings.

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
@yilmaztayfun yilmaztayfun requested review from a team as code owners December 1, 2025 21:04
@yilmaztayfun yilmaztayfun requested review from middt and removed request for a team December 1, 2025 21:04
@sourcery-ai
Copy link

sourcery-ai bot commented Dec 1, 2025

Reviewer's Guide

Adds 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 response

sequenceDiagram
    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
Loading

Class diagram for new HATEOAS pagination components

classDiagram
    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
Loading

File-Level Changes

Change Details Files
Register a pagination link generator service in the ASP.NET Core module.
  • Import pagination namespace into the ASP.NET Core module extensions file.
  • Register IPaginationLinkGenerator with PaginationLinkGenerator as a scoped service in AddAetherAspNetCore.
  • Minor formatting adjustments in exception handling and response compression configuration methods.
framework/src/BBT.Aether.AspNetCore/Microsoft/Extensions/DependencyInjection/AetherAspNetCoreModuleServiceCollectionExtensions.cs
Introduce an HTTP-context-aware PaginationLinkGenerator for building HATEOAS pagination links, including reverse-proxy support.
  • Implement IPaginationLinkGenerator with methods to generate links and create HateoasPagedResultDto for both HateoasPagedList and PagedList.
  • Derive base URL from HttpContext, honoring X-Forwarded-Proto and X-Forwarded-Host headers.
  • Build page URLs with current query string, overriding pagination-related parameters and preserving others.
framework/src/BBT.Aether.AspNetCore/BBT/Aether/Domain/Pagination/PaginationLinkGenerator.cs
Add IQueryable pagination extension methods for generating PagedList and HateoasPagedList instances.
  • Add ToPagedListAsync overloads that compute total count and page metadata, including a PaginationParameters-based overload.
  • Add ToHateoasPagedListAsync overloads that implement N+1 item fetching to determine HasNext without COUNT(*).
  • Normalize pageNumber and pageSize to sane minimums and use PageBy for skip/take operations.
framework/src/BBT.Aether.Infrastructure/BBT/Aether/Domain/QueryableExtensions.cs
Define an abstraction for generating pagination links and HATEOAS results.
  • Create IPaginationLinkGenerator with methods for both HateoasPagedList and PagedList-based link generation.
  • Expose CreateHateoasResult overloads that combine repository results with DTO lists and route paths into HateoasPagedResultDto.
framework/src/BBT.Aether.Application/BBT/Aether/Domain/Pagination/IPaginationLinkGenerator.cs
Add domain and DTO types to support HATEOAS pagination responses.
  • Create HateoasPagedList to represent paged results without TotalCount, tracking page index, size, HasPrevious, HasNext, and items.
  • Create HateoasPagedResultDto to wrap paged DTO items plus PaginationLinks, with a constructor that omits TotalCount for HATEOAS-optimized usage.
  • Create PaginationLinks DTO to hold Self, First, Next, and Prev URLs for pagination navigation.
framework/src/BBT.Aether.Domain/BBT/Aether/Domain/Repositories/HateoasPagedList.cs
framework/src/BBT.Aether.Application/BBT/Aether/Application/Dtos/HateoasPagedResultDto.cs
framework/src/BBT.Aether.Application/BBT/Aether/Application/Dtos/PaginationLinks.cs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@yilmaztayfun yilmaztayfun merged commit f042bc8 into master Dec 1, 2025
3 of 7 checks passed
@coderabbitai
Copy link

coderabbitai bot commented Dec 1, 2025

Caution

Review failed

The pull request is closed.

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'review'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

Introduces HATEOAS pagination support by adding a new DTO for paged results with navigation links, a domain interface for link generation, an ASP.NET Core implementation of the link generator with reverse proxy header support, a lightweight pagination model without total count tracking, and query extensions for efficient database pagination.

Changes

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
Loading

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 IPaginationLinkGenerator are correctly implemented with proper generic constraints and type mapping (TEntity vs TDto).
  • DI registration scope: Confirm AddScoped lifetime is appropriate for IPaginationLinkGenerator given its IHttpContextAccessor dependency.

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

📥 Commits

Reviewing files that changed from the base of the PR and between d0a285b and 1c72682.

📒 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @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

  • HATEOAS Pagination DTOs: Introduced HateoasPagedResultDto<T> and PaginationLinks to structure API responses with navigation links, with an optional total count for performance.
  • Performance-Optimized Paging: Added HateoasPagedList<T> which uses an N+1 strategy to determine if a next page exists, avoiding costly COUNT(*) queries for improved performance.
  • Pagination Link Generation Service: Implemented IPaginationLinkGenerator and PaginationLinkGenerator to dynamically create HATEOAS links based on the current HTTP request, including support for reverse proxy headers.
  • IQueryable Extension Methods: Provided convenient extension methods (ToPagedListAsync, ToHateoasPagedListAsync) to easily convert IQueryable results into paginated lists.
  • Dependency Injection Setup: Configured the PaginationLinkGenerator to be available via dependency injection within the ASP.NET Core module.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@sourcery-ai sourcery-ai bot left a 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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +63 to +64
var pageSize = parameters.MaxResultCount;
var pageNumber = (parameters.SkipCount / pageSize) + 1;
Copy link

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.

Copy link

@gemini-code-assist gemini-code-assist bot left a 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.

Comment on lines +9 to +27
/// <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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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; }

Comment on lines +28 to +85
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);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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>(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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>(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

    public static async Task<PagedList<T>> ToPagedListAsync<T>(

/// <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>(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

    public static async Task<HateoasPagedList<T>> ToHateoasPagedListAsync<T>(

/// <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>(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

    public static async Task<HateoasPagedList<T>> ToHateoasPagedListAsync<T>(


return await query.ToHateoasPagedListAsync(pageNumber, pageSize, cancellationToken);
}
} No newline at end of file

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file does not end with a newline character. It's a common convention to end files with a newline, and some tools and source control systems work better when this convention is followed.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Dec 1, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants