Skip to content

Commit

Permalink
feat(csharp): add pagination methods to C# SDK generator (#5187)
Browse files Browse the repository at this point in the history
feat(csharp): add pagination methods to C# SDK generator
  • Loading branch information
Swimburger authored Nov 19, 2024
1 parent 6ca385c commit 339cc01
Show file tree
Hide file tree
Showing 60 changed files with 3,629 additions and 541 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"python.analysis.typeCheckingMode": "basic"
}
}
7 changes: 7 additions & 0 deletions fern/pages/changelogs/csharp-sdk/2024-11-19.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## 1.9.9
**`(feat):`** Add support for [Auto Pagination](https://buildwithfern.com/learn/sdks/features/auto-pagination).
When enabled, the endpoint methods will return a `Pager<T>` object that you can use to iterate over all items of an endpoint.
Additionally, you can use the `Pager<T>.AsPagesAsync` method to iterate over all pages of an endpoint.
The SDK will automatically make the necessary HTTP requests for you as you iterate over the items or the pages.


74 changes: 44 additions & 30 deletions generators/csharp/codegen/src/AsIs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,47 @@ export const DATETIME_SERIALIZER_CLASS_NAME = "DateTimeSerializer";
export const CONSTANTS_CLASS_NAME = "Constants";
export const JSON_UTILS_CLASS_NAME = "JsonUtils";

export enum AsIsFiles {
GitIgnore = ".gitignore.Template",
CiYaml = "github-ci.yml",
CollectionItemSerializer = "CollectionItemSerializer.cs",
Constants = "Constants.cs",
DateTimeSerializer = "DateTimeSerializer.cs",
EnumConverter = "EnumConverter.Template.cs",
GrpcRequestOptions = "GrpcRequestOptions.Template.cs",
Headers = "Headers.Template.cs",
HeaderValue = "HeaderValue.Template.cs",
HttpMethodExtensions = "HttpMethodExtensions.cs",
JsonConfiguration = "JsonConfiguration.cs",
OneOfSerializer = "OneOfSerializer.cs",
RawClient = "RawClient.Template.cs",
RawClientTests = "RawClientTests.Template.cs",
RawGrpcClient = "RawGrpcClient.Template.cs",
EnumSerializer = "EnumSerializer.Template.cs",
EnumSerializerTests = "EnumSerializerTests.Template.cs",
StringEnum = "StringEnum.Template.cs",
StringEnumExtensions = "StringEnumExtensions.Template.cs",
StringEnumSerializer = "StringEnumSerializer.Template.cs",
StringEnumSerializerTests = "StringEnumSerializerTests.Template.cs",
TemplateCsProj = "Template.csproj",
TemplateTestCsProj = "Template.Test.csproj",
TemplateTestClientCs = "TemplateTestClient.cs",
UsingCs = "Using.cs",
Extensions = "Extensions.cs",
CustomProps = "Custom.props.Template",
TestCustomProps = "Test.Custom.props.Template"
}
export const AsIsFiles = {
CiYaml: "github-ci.yml",
CollectionItemSerializer: "CollectionItemSerializer.cs",
Constants: "Constants.cs",
CustomProps: "Custom.props.Template",
DateTimeSerializer: "DateTimeSerializer.cs",
EnumConverter: "EnumConverter.Template.cs",
EnumSerializer: "EnumSerializer.Template.cs",
Extensions: "Extensions.cs",
GitIgnore: ".gitignore.Template",
GrpcRequestOptions: "GrpcRequestOptions.Template.cs",
Headers: "Headers.Template.cs",
HeaderValue: "HeaderValue.Template.cs",
HttpMethodExtensions: "HttpMethodExtensions.cs",
JsonConfiguration: "JsonConfiguration.cs",
OneOfSerializer: "OneOfSerializer.cs",
Page: "Page.Template.cs",
Pager: "Pager.Template.cs",
RawClient: "RawClient.Template.cs",
RawGrpcClient: "RawGrpcClient.Template.cs",
StringEnum: "StringEnum.Template.cs",
StringEnumExtensions: "StringEnumExtensions.Template.cs",
StringEnumSerializer: "StringEnumSerializer.Template.cs",
TemplateCsProj: "Template.csproj",
UsingCs: "Using.cs",
Test: {
TestCustomProps: "test/Test.Custom.props.Template",
TemplateTestClientCs: "test/TemplateTestClient.cs",
TemplateTestCsProj: "test/Template.Test.csproj",
EnumSerializerTests: "test/EnumSerializerTests.Template.cs",
RawClientTests: "test/RawClientTests.Template.cs",
StringEnumSerializerTests: "test/StringEnumSerializerTests.Template.cs",
Pagination: [
"test/Pagination/GuidCursorTest.Template.cs",
"test/Pagination/HasNextPageOffsetTest.Template.cs",
"test/Pagination/IntOffsetTest.Template.cs",
"test/Pagination/LongOffsetTest.Template.cs",
"test/Pagination/NoRequestCursorTest.Template.cs",
"test/Pagination/NoRequestOffsetTest.Template.cs",
"test/Pagination/StepOffsetTest.Template.cs",
"test/Pagination/StringCursorTest.Template.cs"
]
}
};
19 changes: 19 additions & 0 deletions generators/csharp/codegen/src/asIs/Page.Template.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace <%= namespace%>;

/// <summary>
/// A single <see cref="Page{TItem}"/> of items from a request that may return
/// zero or more <see cref="Page{TItem}"/>s of items.
/// </summary>
/// <typeparam name="TItem">The type of items.</typeparam>
public class Page<TItem>
{
public Page(IReadOnlyList<TItem> items)
{
Items = items;
}

/// <summary>
/// Gets the items in this <see cref="Page{TItem}"/>.
/// </summary>
public IReadOnlyList<TItem> Items { get; }
}
207 changes: 207 additions & 0 deletions generators/csharp/codegen/src/asIs/Pager.Template.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using System.Runtime.CompilerServices;

namespace <%= namespace%>;

/// <summary>
/// A collection of values that may take multiple service requests to
/// iterate over.
/// </summary>
/// <typeparam name="TItem">The type of the values.</typeparam>
public abstract class Pager<TItem> : IAsyncEnumerable<TItem>
{
/// <summary>
/// Enumerate the values a <see cref="Page{TItem}"/> at a time. This may
/// make multiple service requests.
/// </summary>
/// <returns>
/// An async sequence of <see cref="Page{TItem}"/>s.
/// </returns>
public abstract IAsyncEnumerable<Page<TItem>> AsPagesAsync(
CancellationToken cancellationToken = default
);

/// <summary>
/// Enumerate the values in the collection asynchronously. This may
/// make multiple service requests.
/// </summary>
/// <param name="cancellationToken">
/// The <see cref="CancellationToken"/> used for requests made while
/// enumerating asynchronously.
/// </param>
/// <returns>An async sequence of values.</returns>
public virtual async IAsyncEnumerator<TItem> GetAsyncEnumerator(
CancellationToken cancellationToken = default
)
{
await foreach (var page in AsPagesAsync(cancellationToken).ConfigureAwait(false))
{
foreach (var value in page.Items)
{
yield return value;
}
}
}
}

internal sealed class OffsetPager<TRequest, TRequestOptions, TResponse, TOffset, TStep, TItem>
: Pager<TItem>
{
private TRequest _request;
private readonly TRequestOptions? _options;
private readonly GetNextPage _getNextPage;
private readonly GetOffset _getOffset;
private readonly SetOffset _setOffset;
private readonly GetStep? _getStep;
private readonly GetItems _getItems;
private readonly HasNextPage? _hasNextPage;

internal delegate Task<TResponse> GetNextPage(
TRequest request,
TRequestOptions? options,
CancellationToken cancellationToken
);

internal delegate TOffset GetOffset(TRequest request);

internal delegate void SetOffset(TRequest request, TOffset offset);

internal delegate TStep GetStep(TRequest request);

internal delegate IReadOnlyList<TItem>? GetItems(TResponse response);

internal delegate bool? HasNextPage(TResponse response);

internal OffsetPager(
TRequest request,
TRequestOptions? options,
GetNextPage getNextPage,
GetOffset getOffset,
SetOffset setOffset,
GetStep? getStep,
GetItems getItems,
HasNextPage? hasNextPage
)
{
_request = request;
_options = options;
_getNextPage = getNextPage;
_getOffset = getOffset;
_setOffset = setOffset;
_getStep = getStep;
_getItems = getItems;
_hasNextPage = hasNextPage;
}

public override async IAsyncEnumerable<Page<TItem>> AsPagesAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default
)
{
var hasStep = false;
if(_getStep is not null)
{
hasStep = _getStep(_request) is not null;
}
var offset = _getOffset(_request);
var longOffset = Convert.ToInt64(offset);
bool hasNextPage;
do
{
var response = await _getNextPage(_request, _options, cancellationToken)
.ConfigureAwait(false);
var items = _getItems(response);
var itemCount = items?.Count ?? 0;
hasNextPage = _hasNextPage?.Invoke(response) ?? itemCount > 0;
if (items is not null)
{
yield return new Page<TItem>(items);
}

if (hasStep)
{
longOffset += items?.Count ?? 1;
}
else
{
longOffset++;
}

_request ??= Activator.CreateInstance<TRequest>();
switch (offset)
{
case int:
_setOffset(_request, (TOffset)(object)(int)longOffset);
break;
case long:
_setOffset(_request, (TOffset)(object)longOffset);
break;
default:
throw new InvalidOperationException("Offset must be int or long");
}
} while (hasNextPage);
}
}

internal sealed class CursorPager<TRequest, TRequestOptions, TResponse, TCursor, TItem>
: Pager<TItem>
{
private TRequest _request;
private readonly TRequestOptions? _options;
private readonly GetNextPage _getNextPage;
private readonly SetCursor _setCursor;
private readonly GetNextCursor _getNextCursor;
private readonly GetItems _getItems;

internal delegate Task<TResponse> GetNextPage(
TRequest request,
TRequestOptions? options,
CancellationToken cancellationToken
);

internal delegate void SetCursor(TRequest request, TCursor cursor);

internal delegate TCursor? GetNextCursor(TResponse response);

internal delegate IReadOnlyList<TItem>? GetItems(TResponse response);

internal CursorPager(
TRequest request,
TRequestOptions? options,
GetNextPage getNextPage,
SetCursor setCursor,
GetNextCursor getNextCursor,
GetItems getItems
)
{
_request = request;
_options = options;
_getNextPage = getNextPage;
_setCursor = setCursor;
_getNextCursor = getNextCursor;
_getItems = getItems;
}

public override async IAsyncEnumerable<Page<TItem>> AsPagesAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default
)
{
do
{
var response = await _getNextPage(_request, _options, cancellationToken)
.ConfigureAwait(false);
var items = _getItems(response);
var nextCursor = _getNextCursor(response);
if (items != null)
{
yield return new Page<TItem>(items);
}

if (nextCursor == null)
{
break;
}

_request ??= Activator.CreateInstance<TRequest>();
_setCursor(_request, nextCursor);
} while (true);
}
}
Loading

0 comments on commit 339cc01

Please sign in to comment.