-
Notifications
You must be signed in to change notification settings - Fork 22
Implement paging for listing apps and flights #172
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
7a59d02
f21f813
df235bd
9be1ab3
cccd4c1
5bfd1d4
6951553
0f5f4bb
02aec90
7da0006
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 |
|---|---|---|
|
|
@@ -5,7 +5,9 @@ | |
| using System.Collections.Generic; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Globalization; | ||
| using System.Linq; | ||
| using System.Net.Http; | ||
| using System.Runtime.CompilerServices; | ||
| using System.Security.Cryptography.X509Certificates; | ||
| using System.Text; | ||
| using System.Text.Json; | ||
|
|
@@ -219,8 +221,7 @@ public async Task<List<DevCenterApplication>> GetApplicationsAsync(CancellationT | |
| { | ||
| try | ||
| { | ||
| var devCenterApplicationsResponse = await GetDevCenterApplicationsAsync(0, 100, ct); // TODO: pagination | ||
| return devCenterApplicationsResponse.Value ?? []; | ||
| return await GetAllPagesAsync<DevCenterApplication>(GetDevCenterApplicationsAsync, ct).ToListAsync(ct); | ||
| } | ||
| catch (Exception error) | ||
| { | ||
|
|
@@ -384,8 +385,7 @@ public async Task<List<DevCenterFlight>> GetFlightsAsync(string productId, Cance | |
| { | ||
| try | ||
| { | ||
| var devCenterFlightsResponse = await GetFlightsAsync(productId, 0, 100, ct); // TODO: pagination | ||
| return devCenterFlightsResponse.Value ?? []; | ||
| return await GetAllPagesAsync<DevCenterFlight>((skip, top, token) => GetFlightsAsync(productId, skip, top, token), ct).ToListAsync(ct); | ||
| } | ||
| catch (Exception error) | ||
| { | ||
|
|
@@ -663,5 +663,27 @@ public async Task<PackageRollout> FinalizePackageRolloutAsync(string productId, | |
| SourceGenerationContext.GetCustom().PackageRollout, | ||
| ct); | ||
| } | ||
|
|
||
| private static async IAsyncEnumerable<T> GetAllPagesAsync<T>(Func<int, int, CancellationToken, Task<PagedResponse<T>>> pageFunc, [EnumeratorCancellation] CancellationToken ct = default) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Answering your question from the last round on this (Dave Smits (@davesmits) asked whether to keep What tips it is that the helper is now genuinely lazy-safe in a way the earlier version wasn't: One caveat worth writing down. The only reason exceptions still get wrapped in A one-line comment on the helper saying callers must enumerate within the Also minor, now that this is |
||
| { | ||
| int skip = 0; | ||
| const int top = 100; | ||
| PagedResponse<T>? lastPage; | ||
| do | ||
| { | ||
| ct.ThrowIfCancellationRequested(); | ||
|
|
||
| lastPage = await pageFunc(skip, top, ct); | ||
| skip += lastPage.Value?.Count ?? 0; | ||
|
|
||
| foreach (var item in lastPage.Value ?? []) | ||
| { | ||
| ct.ThrowIfCancellationRequested(); | ||
|
|
||
| yield return item; | ||
| } | ||
| } | ||
| while (!string.IsNullOrEmpty(lastPage.NextLink) && lastPage.Value?.Count > 0 && (lastPage.TotalCount <= 0 || skip < lastPage.TotalCount)); | ||
| } | ||
| } | ||
| } | ||
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.
Both call sites materialize immediately with
ToListAsync, and the public signatures still returnTask<List<T>>— so theIAsyncEnumerablemachinery (iterator state machine,[EnumeratorCancellation],System.Linq.AsyncEnumerable) buys us nothing today.There's also a subtle hazard in making this lazy: it only stays inside the
try/catchbecauseToListAsyncis awaited there. If anyone later returns the enumerable to a caller, or adds a.Where(...)that defers enumeration past thetry, exceptions stop getting wrapped inMSStoreExceptionand the error contract silently changes.A plain
private static async Task<List<T>>helper would be simpler and keeps enumeration eagerly bound to the error handling. Happy to defer if you're planning to expose streaming overloads.Uh oh!
There was an error while loading. Please reload this page.
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.
Making it now asyncenumerable gives the consumer the choice what he wants to do, making it more future proof and using logic from the base library (instead of maintaining a own list result); there for went this approach. But yea might be easier to just keep a list. Tell me what you prefer
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.
Fair argument, and I don't feel strongly enough to block on it — your call. Since you asked what I'd prefer: I'd keep the
IAsyncEnumerable, with one caveat.What changed my mind is that you've now made the helper genuinely lazy-friendly (
skipadvances by items actually received, empty-page guard), so it would stream correctly if a caller ever wanted it to. That wasn't true of the earlier version.The caveat is the one I raised: the only reason exceptions still get wrapped in
MSStoreExceptionis thatToListAsyncis awaited inside thetry. That holds today — I checked, both call sites enumerate eagerly inside the block. But it's a tripwire. The day someone returns the enumerable to a caller, or inserts a.Where(...)that defers enumeration past thetry, the wrapping silently stops and the error contract changes with no compiler complaint.If you keep it, worth a short comment on the helper noting that callers must enumerate inside the
tryfor exception wrapping to hold. That way the constraint is written down rather than implicit.Not blocking either way — the
TotalCountissue on the loop condition is the one I'd actually like fixed before this merges.