Skip to content

v1.0.22#59

Merged
yilmaztayfun merged 2 commits into
release-v1.0from
master
Apr 24, 2026
Merged

v1.0.22#59
yilmaztayfun merged 2 commits into
release-v1.0from
master

Conversation

@yilmaztayfun
Copy link
Copy Markdown
Contributor

@yilmaztayfun yilmaztayfun commented Apr 24, 2026

Summary by Sourcery

Introduce a transport-agnostic pagination link generation abstraction and move the HATEOAS pagination link generator from the ASP.NET Core layer into the application layer, with context-specific adapters.

New Features:

  • Add IPaginationLinkGenerator and IPaginationContext abstractions in the application layer for generating HATEOAS pagination links across transports.
  • Provide a default transport-agnostic PaginationLinkGenerator with support for relative and explicit-base-URL link generation.
  • Add HttpPaginationContext to derive pagination context from the current HTTP request, including reverse-proxy aware base URL construction.
  • Expose a protected PaginationLinkGenerator property on ApplicationService for use by application services.

Enhancements:

  • Register a default NullPaginationContext and PaginationLinkGenerator in the Aether application DI setup so non-HTTP hosts generate route-only links by default.
  • Update the ASP.NET Core DI module to replace the default pagination context with the HTTP-aware HttpPaginationContext instead of owning the link generator implementation.

yilmaztayfun and others added 2 commits April 24, 2026 07:36
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.
refactor(pagination): move HATEOAS link generator to application layer
@yilmaztayfun yilmaztayfun self-assigned this Apr 24, 2026
@yilmaztayfun yilmaztayfun requested review from a team April 24, 2026 04:47
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 24, 2026

Reviewer's Guide

Introduce a transport-agnostic pagination link generation abstraction in the Application layer, backed by a pluggable IPaginationContext with a default null implementation and an ASP.NET Core HTTP-aware adapter, and expose it via ApplicationService and DI changes while removing the old ASP.NET Core–specific implementation.

Sequence diagram for HTTP pagination link generation using the new abstractions

sequenceDiagram
    actor User
    participant Controller
    participant ApplicationService
    participant IPaginationLinkGenerator
    participant HttpPaginationContext

    User ->> Controller: HTTP GET /items?page=2&pageSize=10
    Controller ->> ApplicationService: GetItemsAsync(request)
    ApplicationService ->> IPaginationLinkGenerator: CreateHateoasResult(pagedList, routePath)
    IPaginationLinkGenerator ->> HttpPaginationContext: IsAvailable, BaseUrl, QueryParameters
    HttpPaginationContext -->> IPaginationLinkGenerator: scheme, host, pathBase, query
    IPaginationLinkGenerator -->> ApplicationService: HateoasPagedResultDto with PaginationLinks
    ApplicationService -->> Controller: HateoasPagedResultDto
    Controller -->> User: 200 OK with items and HATEOAS pagination links
Loading

Class diagram for new pagination abstractions and implementations

classDiagram
    direction LR

    class ApplicationService {
        <<abstract>>
        +LazyServiceProvider LazyServiceProvider
        +ILoggerFactory LoggerFactory
        +ILogger Logger
        +IClock Clock
        +IPaginationLinkGenerator PaginationLinkGenerator
    }

    class IPaginationContext {
        <<interface>>
        +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 {
        <<interface>>
        +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 PaginationLinks {
        +string Self
        +string First
        +string Next
        +string Prev
    }

    class HateoasPagedList~T~ {
        +int CurrentPage
        +int PageSize
        +bool HasNext
        +bool HasPrevious
    }

    class PagedList~T~ {
        +int CurrentPage
        +int PageSize
        +bool HasNext
        +bool HasPrevious
        +long TotalCount
    }

    class HateoasPagedResultDto~TDto~ {
        +HateoasPagedResultDto(IReadOnlyList~TDto~ items, PaginationLinks links)
        +IReadOnlyList~TDto~ Items
        +PaginationLinks Links
    }

    ApplicationService --> IPaginationLinkGenerator : uses

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

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

    PaginationLinkGenerator --> IPaginationContext : depends on
    FixedBaseUrlPaginationLinkGenerator --> IPaginationContext : depends on

    PaginationLinkGenerator --> PaginationLinks : creates
    FixedBaseUrlPaginationLinkGenerator --> PaginationLinks : creates

    PaginationLinkGenerator --> HateoasPagedList : reads
    PaginationLinkGenerator --> PagedList : reads
    FixedBaseUrlPaginationLinkGenerator --> HateoasPagedList : reads
    FixedBaseUrlPaginationLinkGenerator --> PagedList : reads

    PaginationLinkGenerator --> HateoasPagedResultDto : creates
    FixedBaseUrlPaginationLinkGenerator --> HateoasPagedResultDto : creates

    HateoasPagedResultDto --> PaginationLinks : aggregates
Loading

File-Level Changes

Change Details Files
Add a transport-agnostic pagination link generation service in the Application layer, including context abstraction, default/null implementation, and DI exposure.
  • Introduce IPaginationContext, IPaginationLinkGenerator, PaginationLinkGenerator, FixedBaseUrlPaginationLinkGenerator, and NullPaginationContext in the Application project.
  • Implement link generation that derives base URLs from IPaginationContext, preserves non-pagination query parameters, and supports relative or explicit-base URLs.
  • Expose IPaginationLinkGenerator as a protected LazyServiceProvider-resolved dependency on ApplicationService.
  • Register NullPaginationContext as the default IPaginationContext singleton and PaginationLinkGenerator as the scoped IPaginationLinkGenerator in AddAetherApplication.
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
framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/FixedBaseUrlPaginationLinkGenerator.cs
framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/NullPaginationContext.cs
framework/src/BBT.Aether.Application/BBT/Aether/Application/ApplicationService.cs
framework/src/BBT.Aether.Application/Microsoft/Extensions/DependencyInjection/AetherApplicationModuleServiceCollectionExtensions.cs
Provide an ASP.NET Core–specific IPaginationContext implementation that adapts HTTP requests (including reverse-proxy headers) and wire it into the ASP.NET Core module while removing the old framework-specific link generator.
  • Implement HttpPaginationContext that builds BaseUrl from HttpRequest (honoring X-Forwarded-Proto and X-Forwarded-Host) and exposes the current request query parameters.
  • Replace the default IPaginationContext registration with HttpPaginationContext in the ASP.NET Core DI configuration using ServiceDescriptor.Replace.
  • Remove the old AspNetCore-level IPaginationLinkGenerator and PaginationLinkGenerator in favor of the Application-level implementation.
framework/src/BBT.Aether.AspNetCore/BBT/Aether/AspNetCore/Pagination/HttpPaginationContext.cs
framework/src/BBT.Aether.AspNetCore/Microsoft/Extensions/DependencyInjection/AetherAspNetCoreModuleServiceCollectionExtensions.cs
framework/src/BBT.Aether.AspNetCore/BBT/Aether/AspNetCore/Pagination/IPaginationLinkGenerator.cs
framework/src/BBT.Aether.AspNetCore/BBT/Aether/AspNetCore/Pagination/PaginationLinkGenerator.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

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: babb9c52-a1cd-42ad-bdd4-8658c14b61bc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch master

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 merged commit aed6415 into release-v1.0 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 1 issue, and left some high level feedback:

  • The IPaginationLinkGenerator.Relative XML docs describe route-only links like "users?page=2", but BuildPageLink will emit root-relative links (e.g. "/users?...") or even "?page=..." when routePath is empty; consider either adjusting the implementation or tightening the docs/parameter validation so behavior and comments are aligned.
  • In HttpPaginationContext, BaseUrl silently falls back to an empty string when HttpContext or the Request is unavailable; if this is unexpected in some hosts, it might be safer to expose that situation more explicitly (e.g. via IsAvailable semantics or logging) to make misconfigured IHttpContextAccessor usage easier to diagnose.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `IPaginationLinkGenerator.Relative` XML docs describe route-only links like `"users?page=2"`, but `BuildPageLink` will emit root-relative links (e.g. `"/users?..."`) or even `"?page=..."` when `routePath` is empty; consider either adjusting the implementation or tightening the docs/parameter validation so behavior and comments are aligned.
- In `HttpPaginationContext`, `BaseUrl` silently falls back to an empty string when `HttpContext` or the `Request` is unavailable; if this is unexpected in some hosts, it might be safer to expose that situation more explicitly (e.g. via `IsAvailable` semantics or logging) to make misconfigured `IHttpContextAccessor` usage easier to diagnose.

## Individual Comments

### Comment 1
<location path="framework/src/BBT.Aether.Application/BBT/Aether/Application/Pagination/IPaginationLinkGenerator.cs" line_range="15-16" />
<code_context>
+public interface IPaginationLinkGenerator
+{
+    /// <summary>
+    /// Returns a generator that ignores the ambient transport context and produces
+    /// route-only (relative) links such as <c>"users?page=2&amp;pageSize=10"</c>.
+    /// Query parameters from the context are still preserved.
+    /// </summary>
</code_context>
<issue_to_address>
**issue:** The XML comment for `Relative()` doesn’t match the actual URL shape (root-relative vs. route-only).

`BuildPageLink` always prefixes `/` when `route` is non-empty and `baseUrl` is empty, so the output is actually `"/users?page=2&pageSize=10"`. Please either update the XML doc to describe root-relative links (starting with `/`) or change `BuildPageLink` to omit the leading `/` if you truly intend route-only (no leading slash) URLs.
</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 +15 to +16
/// Returns a generator that ignores the ambient transport context and produces
/// route-only (relative) links such as <c>"users?page=2&amp;pageSize=10"</c>.
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: The XML comment for Relative() doesn’t match the actual URL shape (root-relative vs. route-only).

BuildPageLink always prefixes / when route is non-empty and baseUrl is empty, so the output is actually "/users?page=2&pageSize=10". Please either update the XML doc to describe root-relative links (starting with /) or change BuildPageLink to omit the leading / if you truly intend route-only (no leading slash) URLs.

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 logic by moving it from the AspNetCore layer to the Application layer, making it transport-agnostic. It introduces IPaginationLinkGenerator and IPaginationContext abstractions, allowing for different implementations based on the host environment. Review feedback identifies a security risk in HttpPaginationContext due to manual parsing of forwarded headers, suggesting the use of standard ASP.NET Core middleware instead. Other feedback includes recommendations for null checks in the PaginationLinkGenerator constructor and defensive handling of the BaseUrl property to avoid potential NullReferenceExceptions.

Comment on lines +63 to +83
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;
}

private static string? GetForwardedHost(HttpRequest request)
{
if (request.Headers.TryGetValue("X-Forwarded-Host", out var host) &&
!string.IsNullOrEmpty(host))
{
return host.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.

security-high high

Manual parsing of X-Forwarded-Proto and X-Forwarded-Host headers is generally discouraged in ASP.NET Core. This approach can lead to Host Header Injection vulnerabilities if the application is not behind a trusted proxy, and it bypasses the security features (such as AllowedHosts and KnownProxies) provided by the standard ForwardedHeadersMiddleware.

It is recommended to rely on request.Scheme and request.Host directly. If the application is deployed behind a reverse proxy, the ForwardedHeadersMiddleware should be configured in the application's startup pipeline to correctly populate these properties.

Comment on lines +25 to +28
public PaginationLinkGenerator(IPaginationContext context)
{
_context = context;
}
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

The constructor should validate that the context parameter is not null to prevent a NullReferenceException when the generator is used.

    public PaginationLinkGenerator(IPaginationContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

=> new(items, GenerateLinks(pagedList, routePath));

private string ResolveContextBaseUrl()
=> _context.IsAvailable ? _context.BaseUrl : string.Empty;
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

The BaseUrl property of IPaginationContext might return null depending on the implementation. Since BuildPageLink calls AsSpan() on the result of ResolveContextBaseUrl, a null value would cause a NullReferenceException. Ensuring a non-null string here improves robustness.

        => (_context.IsAvailable ? _context.BaseUrl : string.Empty) ?? string.Empty;

@codacy-production
Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 4 duplication

Metric Results
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

@yilmaztayfun yilmaztayfun changed the title v0.0.22 v1.0.22 Apr 24, 2026
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