A C# source generator library that automatically creates async enumeration methods for cursor and offset-based paginated APIs, making it effortless to iterate through paginated results.
- ✨ Automatic Code Generation: Uses C# source generators to create enumeration methods at compile-time
- 🚀 Zero Runtime Overhead: All code is generated during compilation
- 🔄 IAsyncEnumerable Support: Modern async streaming with
await foreach - 🎯 Type-Safe: Full IntelliSense support and compile-time checking
- 🔌 API Agnostic: Works with any HTTP client library (Refit, HttpClient, etc.)
- ⚙️ Customizable: Configure parameter names to match your API conventions
- 📦 AOT Compatible: Fully compatible with Native AOT compilation
dotnet add package Cursor
Implement the ICursorPage<T> interface for your paginated response:
using Cursor; public class CursorPage<T> : ICursorPage<T> { public List<T> Items { get; set; } = new(); public string? NextCursor { get; set; } }
Add the [GenerateEnumerator] attribute to methods that return paginated results:
using Cursor; using Refit; public interface IExampleApi { [Get("/items")] [GenerateEnumerator] Task<CursorPage<Item>> ListItemsAsync( int? limit = null, string? cursor = null, CancellationToken cancellationToken = default ); }
The source generator automatically creates extension methods for you:
var api = RestService.For<IExampleApi>("https://api.example.com"); // Enumerate all items across all pages await foreach (var item in api.EnumerateItemsAsync()) { Console.WriteLine(item); } // Or collect all items at once var allItems = await api.EnumerateItemsAsync().ToListAsync(); // Enumerate pages instead of individual items await foreach (var page in api.EnumerateItemsPagesAsync()) { Console.WriteLine($"Page with {page.Items.Count} items"); }
Your API methods must:
- Return
Task<TPage>whereTPageimplementsICursorPage<T> - Follow the naming pattern
List*Async(e.g.,ListItemsAsync,ListUsersAsync) - Have parameters for:
limit(or custom name) - page sizecursor(or custom name) - pagination tokencancellationToken- for async cancellation
If your API uses different parameter names, you can customize them:
[Get("/items")] [GenerateEnumerator( LimitParameterName = "pageSize", CursorParameterName = "nextToken" )] Task<CursorPage<Item>> ListItemsAsync( int? pageSize, string? nextToken, CancellationToken cancellationToken ); [Get("/items")] [GenerateEnumerator( CursorParameterName = "offset" )] Task<CursorPage<Item>> ListItemsAsync( int? offset, int? limit, CancellationToken cancellationToken );
You can use any class that implements ICursorPage<T>:
public record CustomCursorPage<T> : ICursorPage<T> { public required List<T> Data { get; init; } public string? Cursor { get; init; } List<T> ICursorPage<T>.Items => Data; string? ICursorPage<T>.NextCursor => Cursor; } [Get("/items")] [GenerateEnumerator] Task<CustomCursorPage<Item>> ListItemsAsync( int? limit, string? cursor = null, CancellationToken cancellationToken = default );
The generator preserves any additional parameters in the generated methods:
[Get("/items")] [GenerateEnumerator] Task<CursorPage<Item>> ListItemsAsync( string category, // Additional parameter int? limit = null, string? cursor = null, CancellationToken cancellationToken = default ); // Usage await foreach (var item in api.EnumerateItemsAsync("electronics")) { // Process items from "electronics" category }
You can create your own attribute that inherits from GenerateEnumeratorAttribute:
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] public class MyCustomEnumeratorAttribute : GenerateEnumeratorAttribute { public MyCustomEnumeratorAttribute() { LimitParameterName = "pageSize"; CursorParameterName = "nextToken"; } } // Usage [Get("/items")] [MyCustomEnumerator] Task<CursorPage<Item>> ListItemsAsync( int? pageSize, string? nextToken, CancellationToken cancellationToken );
For a method named ListItemsAsync, the generator creates:
EnumerateItemsAsync()- ReturnsIAsyncEnumerable<T>for iterating individual itemsEnumerateItemsPagesAsync()- ReturnsIAsyncEnumerable<ICursorPage<T>>for iterating pages
Both methods:
- Automatically handle pagination by following the
NextCursor - Support
CancellationTokenfor graceful cancellation - Allow configuring the page size via an optional
pageSizeparameter
The library uses Roslyn source generators to analyze your code at compile-time:
- Finds methods decorated with
[GenerateEnumerator]or derived attributes - Validates the method signature and return type
- Generates extension methods that handle the pagination loop
- The generated code is added to your compilation automatically
No reflection or runtime code generation is involved, making it AOT-compatible and performant.
using Cursor; using Refit; public interface IGitHubApi { [Get("/users/{user}/repos")] [GenerateEnumerator] Task<CursorPage<Repository>> ListRepositoriesAsync( string user, int? limit = null, string? cursor = null, CancellationToken cancellationToken = default ); } // Usage var api = RestService.For<IGitHubApi>("https://api.github.com"); await foreach (var repo in api.EnumerateRepositoriesAsync("octocat")) { Console.WriteLine($"{repo.Name}: {repo.Description}"); }
await foreach (var page in api.EnumerateRepositoriesPagesAsync("octocat", limit: 50)) { Console.WriteLine($"Processing page with {page.Items.Count} repositories"); // Process entire page at once foreach (var repo in page.Items) { // ... } }
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); try { await foreach (var item in api.EnumerateItemsAsync(cancellationToken: cts.Token)) { // Process item } } catch (OperationCanceledException) { Console.WriteLine("Operation timed out"); }
MIT
Contributions are welcome! Please feel free to submit a Pull Request.