Skip to content

refactor(pagination): move HATEOAS link generator to application layer#58

Merged
yilmaztayfun merged 1 commit into
masterfrom
f/pagination-fix
Apr 24, 2026
Merged

refactor(pagination): move HATEOAS link generator to application layer#58
yilmaztayfun merged 1 commit into
masterfrom
f/pagination-fix

Conversation

@yilmaztayfun
Copy link
Copy Markdown
Contributor

@yilmaztayfun yilmaztayfun commented Apr 24, 2026

⚠️ Breaking change — see Notes section below.

Summary

  • Invert dependency for HATEOAS pagination link generation: the generator and its public contract now live in BBT.Aether.Application, while BBT.Aether.AspNetCore only supplies the HTTP transport adapter.
  • Introduce IPaginationContext so the link-building algorithm stays transport-agnostic and reusable across hosts (HTTP, workers, CLI, tests).
  • Replace the implicit null / empty baseUrl convention with explicit fluent factories — Relative() for root-relative links and WithBaseUrl() for an absolute override.
  • Always emit a leading / between base URL and route, and trim a trailing / from caller-supplied base URLs to avoid //.

Changes

  • Application layer (new):
    • BBT/Aether/Application/Pagination/IPaginationContext.cs
    • BBT/Aether/Application/Pagination/NullPaginationContext.cs
    • BBT/Aether/Application/Pagination/IPaginationLinkGenerator.cs
    • BBT/Aether/Application/Pagination/PaginationLinkGenerator.cs
    • BBT/Aether/Application/Pagination/FixedBaseUrlPaginationLinkGenerator.cs (decorator powering Relative() / WithBaseUrl())
  • Application layer (modified):
    • BBT/Aether/Application/ApplicationService.cs — exposes PaginationLinkGenerator lazy property to all derived services.
    • Microsoft/Extensions/DependencyInjection/AetherApplicationModuleServiceCollectionExtensions.cs — registers NullPaginationContext and PaginationLinkGenerator as the defaults.
  • AspNetCore layer (new):
    • BBT/Aether/AspNetCore/Pagination/HttpPaginationContext.csIHttpContextAccessor-backed adapter with X-Forwarded-Proto / X-Forwarded-Host support.
  • AspNetCore layer (removed):
    • BBT/Aether/AspNetCore/Pagination/IPaginationLinkGenerator.cs
    • BBT/Aether/AspNetCore/Pagination/PaginationLinkGenerator.cs
  • AspNetCore layer (modified):
    • Microsoft/Extensions/DependencyInjection/AetherAspNetCoreModuleServiceCollectionExtensions.cs — replaces IPaginationContext with HttpPaginationContext; the link generator itself is unchanged.

Test Plan

  • `dotnet build framework/src/BBT.Aether.AspNetCore/BBT.Aether.AspNetCore.csproj` succeeds with zero errors.
  • In an AspNetCore host, calling `PaginationLinkGenerator.CreateHateoasResult(...)` returns absolute links honoring `X-Forwarded-Proto` / `X-Forwarded-Host` and preserves non-pagination query parameters.
  • `PaginationLinkGenerator.Relative().GenerateLinks(...)` produces root-relative links starting with `/` (e.g. `/api/core/workflows/.../instances?page=1&pageSize=10`).
  • `PaginationLinkGenerator.WithBaseUrl("https://x.com/\")\` (with trailing slash) does not produce `//` between base and route.
  • In a non-HTTP host (worker / CLI / test), the default registration yields `NullPaginationContext` and links default to root-relative.

Notes

BREAKING CHANGE: `BBT.Aether.AspNetCore.Pagination.IPaginationLinkGenerator` and `PaginationLinkGenerator` are removed. Migration steps for consumers:

  1. Replace `using BBT.Aether.AspNetCore.Pagination;` with `using BBT.Aether.Application.Pagination;`.
  2. Drop any optional `baseUrl` argument from method calls. To get the previous "explicit baseUrl" behavior, chain `.WithBaseUrl("https://...")`. To get the previous "relative link" behavior, chain `.Relative()`.
  3. DI registration changes are handled automatically by `AddAetherApplication()` (default) and `AddAetherAspNetCore()` (HTTP override) — no consumer action required as long as both extension methods are still called.

Made with Cursor

Summary by Sourcery

Move HATEOAS pagination link generation into the application layer behind a transport-agnostic abstraction and adapt ASP.NET Core to supply the HTTP-specific context.

New Features:

  • Introduce a transport-agnostic IPaginationLinkGenerator and IPaginationContext in the application layer for reusable pagination link generation.
  • Add HttpPaginationContext to provide HTTP-aware pagination context, including reverse-proxy header handling.
  • Expose PaginationLinkGenerator from ApplicationService for convenient use in application services.

Bug Fixes:

  • Ensure generated pagination URLs consistently avoid duplicate slashes when combining base URLs and routes and preserve non-pagination query parameters.

Enhancements:

  • Provide fluent configuration for pagination links via Relative() and WithBaseUrl() to control base URL resolution.
  • Register NullPaginationContext and PaginationLinkGenerator by default in the application DI setup, with ASP.NET Core overriding only the pagination context implementation.

Summary by CodeRabbit

Release Notes

  • New Features

    • Pagination link generation now works in non-HTTP environments such as CLI tools, background workers, and tests
  • Refactor

    • Restructured pagination infrastructure for improved modularity and better separation of concerns
    • Enhanced pagination context handling to support custom base URL configuration
    • Separated HTTP-specific pagination logic from core application services

Invert dependency so the link generator lives in BBT.Aether.Application
and AspNetCore only supplies the transport adapter:

- Add IPaginationContext abstraction (BaseUrl, QueryParameters, IsAvailable)
  with NullPaginationContext as the non-HTTP default.
- Add HttpPaginationContext in AspNetCore, backed by IHttpContextAccessor
  and X-Forwarded-* headers; replaces the null context via Replace().
- Move IPaginationLinkGenerator and the link-building algorithm into
  Application; expose it on ApplicationService for shared use across hosts.
- Replace the implicit null/empty baseUrl convention with explicit
  fluent factories: Relative() for root-relative links, WithBaseUrl()
  for an absolute override; FixedBaseUrlPaginationLinkGenerator decorator
  reuses the core builder so URL composition stays in one place.
- Always emit a leading '/' between base URL and route, and trim trailing
  '/' from caller-supplied base URLs to avoid '//'.

BREAKING CHANGE: BBT.Aether.AspNetCore.Pagination.IPaginationLinkGenerator
and PaginationLinkGenerator are removed. Consumers must reference
BBT.Aether.Application.Pagination.IPaginationLinkGenerator instead, and
swap any baseUrl parameter for the Relative() / WithBaseUrl() factories.
@yilmaztayfun yilmaztayfun requested review from a team April 24, 2026 04:45
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 24, 2026

Reviewer's Guide

Refactors HATEOAS pagination so the transport-agnostic link generator and its abstractions live in the Application layer, with a new pagination context abstraction and factories for relative/absolute links, while AspNetCore only provides an HTTP-aware context adapter and updated DI wiring; removes the old AspNetCore-specific generator, making this a breaking change for consumers.

Sequence diagram for HTTP pagination link generation with context override

sequenceDiagram
    actor Client
    participant Controller
    participant ApplicationService
    participant IPaginationLinkGenerator
    participant HttpPaginationContext
    participant IHttpContextAccessor

    Client->>Controller: HTTP GET /items?page=2&pageSize=10
    Controller->>ApplicationService: GetItems(request)

    ApplicationService->>IPaginationLinkGenerator: Relative()
    IPaginationLinkGenerator-->>ApplicationService: IPaginationLinkGenerator (FixedBaseUrlPaginationLinkGenerator)

    ApplicationService->>IPaginationLinkGenerator: CreateHateoasResult(pagedList, items, routePath)
    IPaginationLinkGenerator->>HttpPaginationContext: get QueryParameters
    HttpPaginationContext->>IHttpContextAccessor: HttpContext
    IHttpContextAccessor-->>HttpPaginationContext: HttpContext with Request
    HttpPaginationContext-->>IPaginationLinkGenerator: QueryParameters (from current request)

    IPaginationLinkGenerator->>IPaginationLinkGenerator: BuildLinks(baseUrl="", routePath, currentPage, pageSize, queryParams)
    IPaginationLinkGenerator-->>ApplicationService: HateoasPagedResultDto
    ApplicationService-->>Controller: HateoasPagedResultDto
    Controller-->>Client: 200 OK with HATEOAS pagination links
Loading

Class diagram for new transport-agnostic pagination abstractions

classDiagram
    direction LR

    class IPaginationContext {
        +bool IsAvailable
        +string BaseUrl
        +IReadOnlyList~KeyValuePair~string,string?~~ QueryParameters
    }

    class NullPaginationContext {
        +static NullPaginationContext Instance
        +bool IsAvailable
        +string BaseUrl
        +IReadOnlyList~KeyValuePair~string,string?~~ QueryParameters
    }

    class HttpPaginationContext {
        -IHttpContextAccessor httpContextAccessor
        +bool IsAvailable
        +string BaseUrl
        +IReadOnlyList~KeyValuePair~string,string?~~ QueryParameters
        -string? GetForwardedScheme(HttpRequest request)
        -string? GetForwardedHost(HttpRequest request)
    }

    class IPaginationLinkGenerator {
        +IPaginationLinkGenerator Relative()
        +IPaginationLinkGenerator WithBaseUrl(string baseUrl)
        +PaginationLinks GenerateLinks~T~(HateoasPagedList~T~ pagedList, string routePath)
        +PaginationLinks GenerateLinks~T~(PagedList~T~ pagedList, string routePath)
        +HateoasPagedResultDto~TDto~ CreateHateoasResult~TEntity,TDto~(HateoasPagedList~TEntity~ pagedList, IReadOnlyList~TDto~ items, string routePath)
        +HateoasPagedResultDto~TDto~ CreateHateoasResult~TEntity,TDto~(PagedList~TEntity~ pagedList, IReadOnlyList~TDto~ items, string routePath)
    }

    class PaginationLinkGenerator {
        -IPaginationContext context
        -static HashSet~string~ PaginationKeys
        +PaginationLinkGenerator(IPaginationContext context)
        +IPaginationLinkGenerator Relative()
        +IPaginationLinkGenerator WithBaseUrl(string baseUrl)
        +PaginationLinks GenerateLinks~T~(HateoasPagedList~T~ pagedList, string routePath)
        +PaginationLinks GenerateLinks~T~(PagedList~T~ pagedList, string routePath)
        +HateoasPagedResultDto~TDto~ CreateHateoasResult~TEntity,TDto~(HateoasPagedList~TEntity~ pagedList, IReadOnlyList~TDto~ items, string routePath)
        +HateoasPagedResultDto~TDto~ CreateHateoasResult~TEntity,TDto~(PagedList~TEntity~ pagedList, IReadOnlyList~TDto~ items, string routePath)
        -string ResolveContextBaseUrl()
        +static PaginationLinks BuildLinks(string baseUrl, int currentPage, int pageSize, bool hasNext, bool hasPrevious, string routePath, IReadOnlyList~KeyValuePair~string,string?~~ queryParams)
        -static string BuildPageLink(string baseUrl, string route, int page, int pageSize, IReadOnlyList~KeyValuePair~string,string?~~ queryParams)
    }

    class FixedBaseUrlPaginationLinkGenerator {
        -IPaginationContext context
        -string baseUrl
        +FixedBaseUrlPaginationLinkGenerator(IPaginationContext context, string baseUrl)
        +IPaginationLinkGenerator Relative()
        +IPaginationLinkGenerator WithBaseUrl(string baseUrl)
        +PaginationLinks GenerateLinks~T~(HateoasPagedList~T~ pagedList, string routePath)
        +PaginationLinks GenerateLinks~T~(PagedList~T~ pagedList, string routePath)
        +HateoasPagedResultDto~TDto~ CreateHateoasResult~TEntity,TDto~(HateoasPagedList~TEntity~ pagedList, IReadOnlyList~TDto~ items, string routePath)
        +HateoasPagedResultDto~TDto~ CreateHateoasResult~TEntity,TDto~(PagedList~TEntity~ pagedList, IReadOnlyList~TDto~ items, string routePath)
    }

    class ApplicationService {
        #IPaginationLinkGenerator PaginationLinkGenerator
    }

    IPaginationContext <|.. NullPaginationContext
    IPaginationContext <|.. HttpPaginationContext

    IPaginationLinkGenerator <|.. PaginationLinkGenerator
    IPaginationLinkGenerator <|.. FixedBaseUrlPaginationLinkGenerator

    PaginationLinkGenerator o-- IPaginationContext
    FixedBaseUrlPaginationLinkGenerator o-- IPaginationContext

    ApplicationService o-- IPaginationLinkGenerator
Loading

File-Level Changes

Change Details Files
Introduce transport-agnostic pagination abstractions and implementations in the Application layer and expose them via ApplicationService.
  • Add IPaginationContext, NullPaginationContext, and IPaginationLinkGenerator interfaces to decouple pagination from any specific transport.
  • Implement PaginationLinkGenerator with a shared BuildLinks algorithm that uses IPaginationContext for base URL and query parameters, and supports Relative() and WithBaseUrl(baseUrl) fluent factories.
  • Implement FixedBaseUrlPaginationLinkGenerator as an internal decorator that pins the base URL but still reuses the core link-building logic and preserves context query parameters.
  • Expose a lazily-resolved IPaginationLinkGenerator on ApplicationService so all derived services can generate pagination links without referencing AspNetCore.
BBT.Aether.Application/BBT/Aether/Application/Pagination/IPaginationContext.cs
BBT.Aether.Application/BBT/Aether/Application/Pagination/NullPaginationContext.cs
BBT.Aether.Application/BBT/Aether/Application/Pagination/IPaginationLinkGenerator.cs
BBT.Aether.Application/BBT/Aether/Application/Pagination/PaginationLinkGenerator.cs
BBT.Aether.Application/BBT/Aether/Application/Pagination/FixedBaseUrlPaginationLinkGenerator.cs
BBT.Aether.Application/BBT/Aether/Application/ApplicationService.cs
Wire up default DI registrations for the new pagination components in the Application layer and override the context in AspNetCore with an HTTP-aware implementation.
  • Register NullPaginationContext as the default singleton IPaginationContext and PaginationLinkGenerator as the scoped IPaginationLinkGenerator in AddAetherApplication using TryAdd to allow overrides.
  • Add HttpPaginationContext in AspNetCore as an IPaginationContext implementation backed by IHttpContextAccessor that composes BaseUrl from request scheme/host/pathBase and preserves all query parameters, honoring X-Forwarded-Proto and X-Forwarded-Host.
  • Update AddAetherAspNetCore to Replace the IPaginationContext registration with HttpPaginationContext so HTTP hosts get absolute URLs while non-HTTP hosts remain on NullPaginationContext.
BBT.Aether.Application/Microsoft/Extensions/DependencyInjection/AetherApplicationModuleServiceCollectionExtensions.cs
BBT.Aether.AspNetCore/BBT/Aether/AspNetCore/Pagination/HttpPaginationContext.cs
BBT.Aether.AspNetCore/Microsoft/Extensions/DependencyInjection/AetherAspNetCoreModuleServiceCollectionExtensions.cs
Simplify and harden link composition semantics, including base URL trimming, route joining, and pagination/query parameter handling.
  • Ensure the link builder trims trailing slashes from baseUrl and always inserts a single leading '/' before the route, yielding either absolute or root-relative URLs without '//' duplication.
  • Filter out pagination-related keys (page, pageSize, skipCount, maxResultCount) from preserved query parameters while retaining all others, with proper URI-escaping for keys and values.
  • Standardize relative link behavior so empty base URLs yield root-relative paths (e.g., '/users?...') and multiple values per query parameter are preserved from HttpPaginationContext.
BBT.Aether.Application/BBT/Aether/Application/Pagination/PaginationLinkGenerator.cs
BBT.Aether.AspNetCore/BBT/Aether/AspNetCore/Pagination/HttpPaginationContext.cs
Remove the AspNetCore-specific pagination generator, constituting a breaking change, and migrate consumers to the Application-layer contract.
  • Delete IPaginationLinkGenerator and PaginationLinkGenerator from the AspNetCore layer so pagination is no longer tied to the web transport assembly.
  • Adjust namespaces/usings so consumers and internal code now reference BBT.Aether.Application.Pagination instead of BBT.Aether.AspNetCore.Pagination for pagination link generation.
  • Rely on AddAetherApplication and AddAetherAspNetCore to provide the correct DI setup, so downstream hosts only need to update using directives and optional baseUrl usage to Relative()/WithBaseUrl() chaining.
BBT.Aether.AspNetCore/BBT/Aether/AspNetCore/Pagination/IPaginationLinkGenerator.cs
BBT.Aether.AspNetCore/BBT/Aether/AspNetCore/Pagination/PaginationLinkGenerator.cs
BBT.Aether.AspNetCore/Microsoft/Extensions/DependencyInjection/AetherAspNetCoreModuleServiceCollectionExtensions.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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5dd53b1b-2a01-48b2-af0f-7b0732e9a9ef

📥 Commits

Reviewing files that changed from the base of the PR and between 8ab8f8e and dd4a747.

📒 Files selected for processing (11)
  • framework/src/BBT.Aether.Application/BBT/Aether/Application/ApplicationService.cs
  • framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/FixedBaseUrlPaginationLinkGenerator.cs
  • framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/IPaginationContext.cs
  • framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/IPaginationLinkGenerator.cs
  • framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/NullPaginationContext.cs
  • framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/PaginationLinkGenerator.cs
  • framework/src/BBT.Aether.Application/Microsoft/Extensions/DependencyInjection/AetherApplicationModuleServiceCollectionExtensions.cs
  • framework/src/BBT.Aether.AspNetCore/BBT/Aether/AspNetCore/Pagination/HttpPaginationContext.cs
  • framework/src/BBT.Aether.AspNetCore/BBT/Aether/AspNetCore/Pagination/IPaginationLinkGenerator.cs
  • framework/src/BBT.Aether.AspNetCore/BBT/Aether/AspNetCore/Pagination/PaginationLinkGenerator.cs
  • framework/src/BBT.Aether.AspNetCore/Microsoft/Extensions/DependencyInjection/AetherAspNetCoreModuleServiceCollectionExtensions.cs

📝 Walkthrough

Walkthrough

This change relocates pagination abstractions from AspNetCore to the Application layer by introducing a transport-agnostic IPaginationContext interface. The existing PaginationLinkGenerator logic is refactored to depend on context instead of direct HTTP access, and an HTTP-specific HttpPaginationContext adapter is added to AspNetCore. A new FixedBaseUrlPaginationLinkGenerator variant and NullPaginationContext default implementation are introduced, with updated DI registrations across both modules.

Changes

Cohort / File(s) Summary
Application Layer - Pagination Abstractions
framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/IPaginationContext.cs, framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/IPaginationLinkGenerator.cs, framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/PaginationLinkGenerator.cs
New transport-agnostic pagination abstractions: IPaginationContext interface defining availability, base URL, and query parameters; IPaginationLinkGenerator interface for HATEOAS link generation; and PaginationLinkGenerator implementation delegating to injected context instead of HTTP access.
Application Layer - Pagination Context Implementations
framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/NullPaginationContext.cs, framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/FixedBaseUrlPaginationLinkGenerator.cs
Default and fixed-URL variants: NullPaginationContext provides empty/unavailable defaults for non-HTTP hosts; FixedBaseUrlPaginationLinkGenerator pins base URL to caller-supplied value, supporting relative and custom base URL modes.
Application Layer - ServiceCollection Integration
framework/src/BBT.Aether.Application/BBT/Aether/Application/ApplicationService.cs, framework/src/BBT.Aether.Application/Microsoft/Extensions/DependencyInjection/AetherApplicationModuleServiceCollectionExtensions.cs
Adds PaginationLinkGenerator protected dependency accessor to ApplicationService; registers default IPaginationContext and IPaginationLinkGenerator services via conditional TryAdd* calls.
AspNetCore Layer - HTTP Adapter
framework/src/BBT.Aether.AspNetCore/BBT/Aether/AspNetCore/Pagination/HttpPaginationContext.cs
New HTTP-aware IPaginationContext implementation that computes availability, base URL (with reverse-proxy header support), and query parameters from current HTTP context.
AspNetCore Layer - DI and Cleanup
framework/src/BBT.Aether.AspNetCore/Microsoft/Extensions/DependencyInjection/AetherAspNetCoreModuleServiceCollectionExtensions.cs
Updates DI registration to replace default IPaginationContext with HttpPaginationContext instead of registering PaginationLinkGenerator directly; old interface and implementation files removed from AspNetCore.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • tsimsekburgan
  • ukaratas

Poem

🐰 Hops through layers, light and free,
Contexts now are agile—see?
HTTP-aware, or null with grace,
Pagination links find their place! 🎯

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch f/pagination-fix

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.

@yilmaztayfun yilmaztayfun self-assigned this Apr 24, 2026
@yilmaztayfun yilmaztayfun merged commit a237853 into master Apr 24, 2026
4 of 6 checks passed
Copy link
Copy Markdown

@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 - I've found 2 issues, and left some high level feedback:

  • The XML doc on ApplicationService.PaginationLinkGenerator still refers to overriding behavior by passing a baseUrl argument, which no longer exists on the public API; update this comment to describe using Relative()/WithBaseUrl instead.
  • The documentation for IPaginationLinkGenerator.Relative suggests route-only links like "users?...", but BuildPageLink always prepends "/" (root-relative); consider aligning the comment and examples with the actual leading-slash behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The XML doc on ApplicationService.PaginationLinkGenerator still refers to overriding behavior by passing a baseUrl argument, which no longer exists on the public API; update this comment to describe using Relative()/WithBaseUrl instead.
- The documentation for IPaginationLinkGenerator.Relative suggests route-only links like "users?...", but BuildPageLink always prepends "/" (root-relative); consider aligning the comment and examples with the actual leading-slash behavior.

## Individual Comments

### Comment 1
<location path="framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/PaginationLinkGenerator.cs" line_range="106-118" />
<code_context>
+        // (e.g. when callers pass "https://x.com/" via WithBaseUrl).
+        sb.Append(baseUrl.AsSpan().TrimEnd('/'));
+
+        if (!string.IsNullOrEmpty(route))
+        {
+            // Always separate base URL from route with '/'. When baseUrl is empty,
+            // this produces a root-relative link (e.g. "/users?...") which browsers
+            // and HTTP clients resolve consistently regardless of current path.
+            sb.Append('/');
+            sb.Append(route);
+        }
+
</code_context>
<issue_to_address>
**suggestion:** Relative links are root-relative (start with '/') rather than pure route-only as the XML docs suggest

When `baseUrl` is empty (e.g. via `Relative()`), the generator still prefixes routes with `'/'`, so `"users"` becomes `"/users?..."` rather than `"users?..."`. The XML docs for `Relative()` and `IPaginationLinkGenerator` describe route-only links, which doesn’t match this behavior. Please either drop the leading `'/'` when `baseUrl` is empty, or update the XML docs/naming to clarify that `Relative()` returns root-relative links.

```suggestion
        var sb = new StringBuilder();
        // Trim a trailing '/' from baseUrl so we never produce '//' between base and route
        // (e.g. when callers pass "https://x.com/" via WithBaseUrl).
        var trimmedBaseUrl = baseUrl.AsSpan().TrimEnd('/');
        if (trimmedBaseUrl.Length > 0)
        {
            sb.Append(trimmedBaseUrl);
        }

        if (!string.IsNullOrEmpty(route))
        {
            // When a non-empty base URL is present, separate it from the route with '/'.
            // When baseUrl is empty (e.g. Relative()), emit a route-only link ("users?..."),
            // matching the XML documentation for IPaginationLinkGenerator.
            if (sb.Length > 0)
            {
                sb.Append('/');
            }

            sb.Append(route);
        }
```
</issue_to_address>

### Comment 2
<location path="framework/src/BBT.Aether.AspNetCore/BBT/Aether/AspNetCore/Pagination/HttpPaginationContext.cs" line_range="63-71" />
<code_context>
+        }
+    }
+
+    private static string? GetForwardedScheme(HttpRequest request)
+    {
+        if (request.Headers.TryGetValue("X-Forwarded-Proto", out var proto) &&
+            !string.IsNullOrEmpty(proto))
+        {
+            return proto.ToString().Split(',')[0].Trim();
+        }
+
+        return null;
+    }
+
</code_context>
<issue_to_address>
**🚨 issue (security):** Using X-Forwarded-* headers directly can be unsafe if proxies are not trusted or not consistently configured

`BaseUrl` currently prefers `X-Forwarded-Proto` / `X-Forwarded-Host` whenever present. In environments where the app is also directly reachable or the proxy isn’t fully trusted/sanitizing, clients can spoof these headers and affect generated links (e.g., host/scheme in pagination or Location-like URLs). If this helper is intended to be reusable, consider making use of `X-Forwarded-*` opt-in or wiring it to existing trust configuration (e.g., `ForwardedHeadersOptions` / `UseForwardedHeaders`) so these headers are only honored when explicitly trusted.
</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 +106 to +118
var sb = new StringBuilder();
// Trim a trailing '/' from baseUrl so we never produce '//' between base and route
// (e.g. when callers pass "https://x.com/" via WithBaseUrl).
sb.Append(baseUrl.AsSpan().TrimEnd('/'));

if (!string.IsNullOrEmpty(route))
{
// Always separate base URL from route with '/'. When baseUrl is empty,
// this produces a root-relative link (e.g. "/users?...") which browsers
// and HTTP clients resolve consistently regardless of current path.
sb.Append('/');
sb.Append(route);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Relative links are root-relative (start with '/') rather than pure route-only as the XML docs suggest

When baseUrl is empty (e.g. via Relative()), the generator still prefixes routes with '/', so "users" becomes "/users?..." rather than "users?...". The XML docs for Relative() and IPaginationLinkGenerator describe route-only links, which doesn’t match this behavior. Please either drop the leading '/' when baseUrl is empty, or update the XML docs/naming to clarify that Relative() returns root-relative links.

Suggested change
var sb = new StringBuilder();
// Trim a trailing '/' from baseUrl so we never produce '//' between base and route
// (e.g. when callers pass "https://x.com/" via WithBaseUrl).
sb.Append(baseUrl.AsSpan().TrimEnd('/'));
if (!string.IsNullOrEmpty(route))
{
// Always separate base URL from route with '/'. When baseUrl is empty,
// this produces a root-relative link (e.g. "/users?...") which browsers
// and HTTP clients resolve consistently regardless of current path.
sb.Append('/');
sb.Append(route);
}
var sb = new StringBuilder();
// Trim a trailing '/' from baseUrl so we never produce '//' between base and route
// (e.g. when callers pass "https://x.com/" via WithBaseUrl).
var trimmedBaseUrl = baseUrl.AsSpan().TrimEnd('/');
if (trimmedBaseUrl.Length > 0)
{
sb.Append(trimmedBaseUrl);
}
if (!string.IsNullOrEmpty(route))
{
// When a non-empty base URL is present, separate it from the route with '/'.
// When baseUrl is empty (e.g. Relative()), emit a route-only link ("users?..."),
// matching the XML documentation for IPaginationLinkGenerator.
if (sb.Length > 0)
{
sb.Append('/');
}
sb.Append(route);
}

Comment on lines +63 to +71
private static string? GetForwardedScheme(HttpRequest request)
{
if (request.Headers.TryGetValue("X-Forwarded-Proto", out var proto) &&
!string.IsNullOrEmpty(proto))
{
return proto.ToString().Split(',')[0].Trim();
}

return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 issue (security): Using X-Forwarded-* headers directly can be unsafe if proxies are not trusted or not consistently configured

BaseUrl currently prefers X-Forwarded-Proto / X-Forwarded-Host whenever present. In environments where the app is also directly reachable or the proxy isn’t fully trusted/sanitizing, clients can spoof these headers and affect generated links (e.g., host/scheme in pagination or Location-like URLs). If this helper is intended to be reusable, consider making use of X-Forwarded-* opt-in or wiring it to existing trust configuration (e.g., ForwardedHeadersOptions / UseForwardedHeaders) so these headers are only honored when explicitly trusted.

Copy link
Copy Markdown

@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 refactors the HATEOAS pagination link generation to be transport-agnostic by moving the core logic from the AspNetCore layer to the Application layer. It introduces an IPaginationContext abstraction, allowing the generator to resolve base URLs and query parameters from various sources, such as HTTP requests or background workers. Feedback provided focuses on improving the robustness of the PaginationLinkGenerator by ensuring that potential null values for routePath and baseUrl are handled safely to prevent runtime exceptions.

string routePath,
IReadOnlyList<KeyValuePair<string, string?>> queryParams)
{
var route = routePath.TrimStart('/');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

routePath.TrimStart('/') will throw a NullReferenceException if routePath is null. While this parameter is typically provided as a string literal, it is safer to handle potential null values to avoid runtime errors.

        var route = (routePath ?? string.Empty).TrimStart('/');

Comment on lines +106 to +109
var sb = new StringBuilder();
// Trim a trailing '/' from baseUrl so we never produce '//' between base and route
// (e.g. when callers pass "https://x.com/" via WithBaseUrl).
sb.Append(baseUrl.AsSpan().TrimEnd('/'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This block can be optimized for performance and robustness. Initializing the StringBuilder with an estimated capacity reduces reallocations. Additionally, baseUrl.AsSpan() will throw if baseUrl is null; using a null-coalescing operator ensures the generator remains resilient even if the transport context returns a null base URL.

        var sb = new StringBuilder((baseUrl?.Length ?? 0) + (route?.Length ?? 0) + 128);
        // Trim a trailing '/' from baseUrl so we never produce '//' between base and route
        // (e.g. when callers pass "https://x.com/" via WithBaseUrl).
        sb.Append((baseUrl ?? string.Empty).AsSpan().TrimEnd('/'));

@codacy-production
Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 24 complexity · 4 duplication

Metric Results
Complexity 24
Duplication 4

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)
D Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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.

1 participant