-
Notifications
You must be signed in to change notification settings - Fork 0
Add pagination support to list endpoints #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5753c2d
be749e5
48eed44
68bfe32
4547648
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| # Pagination Support | ||
|
|
||
| ## Overview | ||
|
|
||
| The MythAPI now supports pagination for all list-returning endpoints. This improves performance and usability when dealing with large datasets. | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Query Parameters | ||
|
|
||
| All list endpoints now accept optional pagination parameters: | ||
|
|
||
| - `page` - The page number (1-based). Default: returns all results if not specified | ||
| - `pageSize` - The number of items per page. Default: 10, Maximum: 100 | ||
|
|
||
| ### Examples | ||
|
|
||
| #### Without Pagination (Backward Compatible) | ||
| ```bash | ||
| # Returns all gods | ||
| GET /api/v1/gods | ||
|
|
||
| # Returns all mythologies | ||
| GET /api/v1/mythologies | ||
| ``` | ||
|
|
||
| #### With Pagination | ||
| ```bash | ||
| # Returns first page with 10 items (default page size) | ||
| GET /api/v1/gods?page=1 | ||
|
|
||
| # Returns second page with 5 items per page | ||
| GET /api/v1/gods?page=2&pageSize=5 | ||
|
|
||
| # Returns first page of mythologies with 20 items | ||
| GET /api/v1/mythologies?page=1&pageSize=20 | ||
| ``` | ||
|
|
||
| ## Response Format | ||
|
|
||
| ### Non-Paginated Response | ||
| When pagination parameters are not provided, the response is a simple list: | ||
| ```json | ||
| [ | ||
| { | ||
| "id": 1, | ||
| "name": "Zeus", | ||
| "description": "God of the sky", | ||
| "mythologyId": 1, | ||
| "aliases": [] | ||
| }, | ||
| ... | ||
| ] | ||
| ``` | ||
|
|
||
| ### Paginated Response | ||
| When pagination parameters are provided, the response includes metadata: | ||
| ```json | ||
| { | ||
| "items": [ | ||
| { | ||
| "id": 1, | ||
| "name": "Zeus", | ||
| "description": "God of the sky", | ||
| "mythologyId": 1, | ||
| "aliases": [] | ||
| }, | ||
| ... | ||
| ], | ||
| "page": 1, | ||
| "pageSize": 10, | ||
| "totalCount": 42, | ||
| "totalPages": 5, | ||
| "hasNext": true, | ||
| "hasPrevious": false | ||
| } | ||
| ``` | ||
|
|
||
| ### Response Fields | ||
|
|
||
| - `items` - Array of items for the current page | ||
| - `page` - Current page number (1-based) | ||
| - `pageSize` - Number of items per page | ||
| - `totalCount` - Total number of items across all pages | ||
| - `totalPages` - Total number of pages available | ||
| - `hasNext` - Boolean indicating if there's a next page | ||
| - `hasPrevious` - Boolean indicating if there's a previous page | ||
|
|
||
| ## Validation | ||
|
|
||
| The API automatically validates and corrects invalid pagination parameters: | ||
|
|
||
| - `page` values less than 1 are automatically set to 1 | ||
| - `pageSize` values greater than 100 are automatically capped at 100 | ||
| - `pageSize` values less than 1 are automatically set to the default (10) | ||
|
|
||
| ## Endpoints Supporting Pagination | ||
|
|
||
| The following endpoints support pagination: | ||
|
|
||
| 1. **GET /api/v1/gods** - List all gods | ||
| 2. **GET /api/v1/mythologies** - List all mythologies | ||
|
|
||
| ## Implementation Notes | ||
|
|
||
| - Pagination is implemented at the database level for optimal performance | ||
| - Uses Entity Framework's `Skip()` and `Take()` methods | ||
| - Eager loading is used to avoid N+1 query problems | ||
| - Results are ordered by ID to ensure consistent pagination | ||
|
|
||
| ## Backward Compatibility | ||
|
|
||
| Existing clients that don't provide pagination parameters will continue to work as before, receiving the complete list of items. This ensures no breaking changes for existing integrations. | ||
|
|
||
| ## Best Practices | ||
|
|
||
| 1. **Use appropriate page sizes**: Balance between reducing the number of requests and keeping response sizes manageable | ||
| 2. **Handle edge cases**: Always check `hasNext` and `hasPrevious` before navigating | ||
| 3. **Cache when possible**: If data doesn't change frequently, consider caching paginated results | ||
| 4. **Consistent parameters**: Use the same page size across related requests for better caching | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,43 @@ | ||||||
| namespace MythApi.Common.Models; | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Represents a paginated result set | ||||||
| /// </summary> | ||||||
| /// <typeparam name="T">The type of items in the result</typeparam> | ||||||
| public class PagedResult<T> | ||||||
| { | ||||||
| /// <summary> | ||||||
| /// The items in the current page | ||||||
| /// </summary> | ||||||
| public IList<T> Items { get; set; } = new List<T>(); | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Current page number (1-based) | ||||||
| /// </summary> | ||||||
| public int Page { get; set; } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Number of items per page | ||||||
| /// </summary> | ||||||
| public int PageSize { get; set; } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Total number of items across all pages | ||||||
| /// </summary> | ||||||
| public int TotalCount { get; set; } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Total number of pages | ||||||
| /// </summary> | ||||||
| public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize); | ||||||
|
||||||
| public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize); | |
| public int TotalPages => PageSize <= 0 ? 0 : (int)Math.Ceiling(TotalCount / (double)PageSize); |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The HasNext calculation (Page < TotalPages) will incorrectly return true when there are 0 items total. For example, if TotalCount=0 and PageSize=10, then TotalPages=0, and if Page=1, then HasNext would be 1 < 0 = false (correct). However, if Page=0 (which shouldn't happen but could if PagedResult is constructed without PaginationParameters), HasNext would be 0 < 0 = false. More critically, when TotalCount=0, TotalPages=0, and the current Page=1, the calculation is correct but the semantic meaning might be confusing. Consider adding a guard: HasNext => Page < TotalPages && TotalCount > 0 for clarity.
| public bool HasNext => Page < TotalPages; | |
| public bool HasNext => TotalCount > 0 && Page < TotalPages; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| namespace MythApi.Common.Models; | ||
|
|
||
| /// <summary> | ||
| /// Parameters for pagination requests | ||
| /// </summary> | ||
| public class PaginationParameters | ||
| { | ||
| private const int MaxPageSize = 100; | ||
| private const int DefaultPageSize = 10; | ||
|
|
||
| private int _pageSize = DefaultPageSize; | ||
| private int _page = 1; | ||
|
|
||
| /// <summary> | ||
| /// Page number (1-based) | ||
| /// </summary> | ||
| public int Page | ||
| { | ||
| get => _page; | ||
| set => _page = value < 1 ? 1 : value; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Number of items per page (default: 10, max: 100) | ||
| /// </summary> | ||
| public int PageSize | ||
| { | ||
| get => _pageSize; | ||
| set => _pageSize = value > MaxPageSize ? MaxPageSize : (value < 1 ? DefaultPageSize : value); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Calculate the number of items to skip | ||
| /// </summary> | ||
| public int Skip => (Page - 1) * PageSize; | ||
| } | ||
|
Comment on lines
+1
to
+36
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using MythApi.Gods.Interfaces; | ||
| using MythApi.Common.Database.Models; | ||
| using MythApi.Common.Models; | ||
| using MythApi.Gods.Models; | ||
|
|
||
| namespace MythApi.Endpoints.v1; | ||
|
|
@@ -10,13 +11,34 @@ public static void RegisterGodEndpoints(this IEndpointRouteBuilder endpoints) { | |
| var gods = endpoints.MapGroup("/api/v1/gods"); | ||
|
|
||
|
|
||
| gods.MapGet("", GetAlllGods); | ||
| gods.MapGet("", GetAllGods); | ||
| gods.MapGet("{id}", (int id, IGodRepository repository) => repository.GetGodAsync(new GodParameter(id))); | ||
| gods.MapGet("search/{name}", (string name, IGodRepository repository, [FromQuery] bool includeAliases = false) => repository.GetGodByNameAsync(new GodByNameParameter(name, includeAliases))); | ||
| gods.MapPost("", AddOrUpdateGods); | ||
| } | ||
|
|
||
| public static Task<List<God>> AddOrUpdateGods(List<GodInput> gods, IGodRepository repository) => repository.AddOrUpdateGods(gods); | ||
|
|
||
| public static Task<IList<God>> GetAlllGods(IGodRepository repository) => repository.GetAllGodsAsync(); | ||
| public static async Task<IResult> GetAllGods( | ||
| IGodRepository repository, | ||
| [FromQuery] int? page = null, | ||
| [FromQuery] int? pageSize = null) | ||
| { | ||
| // If pagination parameters are not provided, return all gods (backward compatibility) | ||
| if (page == null && pageSize == null) | ||
| { | ||
| var allGods = await repository.GetAllGodsAsync(); | ||
| return Results.Ok(allGods); | ||
| } | ||
|
Comment on lines
+27
to
+32
|
||
|
|
||
| // Use pagination | ||
| var pagination = new PaginationParameters | ||
| { | ||
| Page = page ?? 1, | ||
| PageSize = pageSize ?? 10 | ||
| }; | ||
|
|
||
| var pagedResult = await repository.GetAllGodsAsync(pagination); | ||
| return Results.Ok(pagedResult); | ||
| } | ||
|
Comment on lines
+22
to
+43
|
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,6 @@ | ||||||||||||||||||||||||||||||||||
| using Microsoft.AspNetCore.Mvc; | ||||||||||||||||||||||||||||||||||
| using MythApi.Common.Database.Models; | ||||||||||||||||||||||||||||||||||
| using MythApi.Common.Models; | ||||||||||||||||||||||||||||||||||
| using MythApi.Mythologies.Interfaces; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| namespace MythApi.Endpoints.v1; |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The backward compatibility logic has an inconsistency: providing only 'page' (without 'pageSize') or only 'pageSize' (without 'page') will trigger pagination, but the PR description states pagination should be optional. This means GET /api/v1/mythologies?page=1 will use pagination with default pageSize=10, while GET /api/v1/mythologies will return all results. This could be confusing for API consumers. Consider clarifying the intended behavior: should ANY pagination parameter trigger pagination, or should BOTH be required?
| // If pagination parameters are not provided, return all mythologies (backward compatibility) | |
| if (page == null && pageSize == null) | |
| { | |
| var allMythologies = await repository.GetAllMythologiesAsync(); | |
| return Results.Ok(allMythologies); | |
| } | |
| // Use pagination | |
| // Backward compatibility: if neither pagination parameter is provided, return all mythologies | |
| if (page == null && pageSize == null) | |
| { | |
| var allMythologies = await repository.GetAllMythologiesAsync(); | |
| return Results.Ok(allMythologies); | |
| } | |
| // Use pagination whenever at least one pagination parameter is provided; default missing values |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR description mentions "Updates to OpenAPI/Swagger documentation to reflect changes" as part of the acceptance criteria, but there are no code changes to configure Swagger/OpenAPI annotations for the new pagination parameters. The endpoint signatures have changed, but without explicit Swagger attributes (e.g., ProducesResponseType, SwaggerOperation), the auto-generated Swagger documentation may not clearly show that endpoints can return either IList or PagedResult depending on parameters. Consider adding Swagger annotations to document the different response types and parameter descriptions.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| using MythApi.Gods.Interfaces; | ||
| using MythApi.Common.Database.Models; | ||
| using MythApi.Common.Database; | ||
| using MythApi.Common.Models; | ||
| using MythApi.Gods.Models; | ||
|
|
||
| namespace MythApi.Gods.DBRepositories; | ||
|
|
@@ -44,14 +45,32 @@ public async Task<List<God>> AddOrUpdateGods(List<GodInput> gods) | |
|
|
||
| public async Task<IList<God>> GetAllGodsAsync() | ||
| { | ||
| var gods = await _context.Gods.ToListAsync(); | ||
| foreach (var god in gods) | ||
| { | ||
| _context.Entry(god).Collection(x => x.Aliases).Load(); | ||
| } | ||
| var gods = await _context.Gods | ||
| .Include(g => g.Aliases) | ||
| .ToListAsync(); | ||
| return gods; | ||
| } | ||
|
|
||
| public async Task<PagedResult<God>> GetAllGodsAsync(PaginationParameters pagination) | ||
| { | ||
| var totalCount = await _context.Gods.CountAsync(); | ||
|
|
||
| var gods = await _context.Gods | ||
| .Include(g => g.Aliases) | ||
| .OrderBy(g => g.Id) | ||
| .Skip(pagination.Skip) | ||
| .Take(pagination.PageSize) | ||
| .ToListAsync(); | ||
|
Comment on lines
+56
to
+63
|
||
|
|
||
| return new PagedResult<God> | ||
| { | ||
| Items = gods, | ||
| Page = pagination.Page, | ||
| PageSize = pagination.PageSize, | ||
| TotalCount = totalCount | ||
| }; | ||
| } | ||
|
|
||
| public async Task<God> GetGodAsync(GodParameter parameter) | ||
| { | ||
| return await _context.Gods.FirstAsync(x => x.Id == parameter.Id); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||
| using MythApi.Gods.Interfaces; | ||||||||
| using MythApi.Common.Database.Models; | ||||||||
| using MythApi.Common.Models; | ||||||||
| using MythApi.Gods.Models; | ||||||||
|
|
||||||||
| namespace MythApi.Gods.Mocks; | ||||||||
|
|
@@ -40,6 +41,23 @@ public Task<IList<God>> GetAllGodsAsync() | |||||||
| return Task.FromResult(gods as IList<God>); | ||||||||
| } | ||||||||
|
|
||||||||
| public Task<PagedResult<God>> GetAllGodsAsync(PaginationParameters pagination) | ||||||||
| { | ||||||||
| var totalCount = gods.Count; | ||||||||
| var items = gods | ||||||||
|
||||||||
| var items = gods | |
| var items = gods | |
| .OrderBy(g => g.Id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation on line 30 shows GET /api/v1/gods?page=1 as an example, which contradicts the backward compatibility statement on line 113. According to the implementation, providing only 'page' (without 'pageSize') will trigger pagination with a default pageSize of 10, not return all results. Either update the example to include both parameters (GET /api/v1/gods?page=1&pageSize=10) or clarify in the documentation that providing ANY pagination parameter triggers pagination mode.