Description
Background and motivation
Overview
The System.ClientModel library provides building blocks for .NET clients that call cloud services. System.ClientModel 1.0.0 has types for reading and writing message content, reviewed as part of #94126, and System.ClientModel 1.1.0-beta.2 added end-user convenience types and client-author pipeline types, reviewed as part of #97711.
In this iteration of the System.ClientModel API, we would like to add types to enable clients to provide service methods to retrieve collections of items from paginated endpoints and streaming endpoints.
Page Collection Types
Cloud services use pagination to return a collection of items over multiple responses. Each response from the service returns a page of items in the collection, as well as the information needed to obtain the next page of items, until all the items in the requested collection have been returned. There are two primary client-user scenarios for this category of service methods: enumerating all the items of the requested collection (independent of the paged delivery mechanism) and rendering a single page of results. In addition to these primary scenarios, there is a requirement that the collection must be able to be "rehydrated," i.e. continued from a process different from the one that originally created the collection. The rehydration requirement means that we must be able to persist the collection state and separately recreate the collection from the persisted state.
To enable clients to provide service methods for paginated endpoints, we would like to add PageCollection<T>
, AsyncPageCollection<T>
, PageResult<T>
and ContinuationToken
types as described below.
Streaming Collection Types
Cloud services such as OpenAI and Azure OpenAI use SSE streams to deliver incremental updates sequentially over a single response stream. We would like to add CollectionResult<T>
and AsyncCollectionResult<T>
types to System.ClientModel to provide a convenience layer over the SseParser types in the System.Formats.Sse
namespace. This will enable clients to provide service methods that return collections of model types and from which the raw HTTP response details can also be obtained.
API Proposal
namespace System.ClientModel
{
public abstract partial class AsyncCollectionResult<T> : System.ClientModel.ClientResult, System.Collections.Generic.IAsyncEnumerable<T>
{
protected internal AsyncCollectionResult() { }
protected internal AsyncCollectionResult(System.ClientModel.Primitives.PipelineResponse response) { }
public abstract System.Collections.Generic.IAsyncEnumerator<T> GetAsyncEnumerator(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
}
public abstract partial class AsyncPageCollection<T> : System.Collections.Generic.IAsyncEnumerable<System.ClientModel.PageResult<T>>
{
protected AsyncPageCollection() { }
public System.Collections.Generic.IAsyncEnumerable<T> GetAllValuesAsync([System.Runtime.CompilerServices.EnumeratorCancellationAttribute] System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
protected abstract System.Collections.Generic.IAsyncEnumerator<System.ClientModel.PageResult<T>> GetAsyncEnumeratorCore(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
public System.Threading.Tasks.Task<System.ClientModel.PageResult<T>> GetCurrentPageAsync() { throw null; }
protected abstract System.Threading.Tasks.Task<System.ClientModel.PageResult<T>> GetCurrentPageAsyncCore();
System.Collections.Generic.IAsyncEnumerator<System.ClientModel.PageResult<T>> System.Collections.Generic.IAsyncEnumerable<System.ClientModel.PageResult<T>>.GetAsyncEnumerator(System.Threading.CancellationToken cancellationToken) { throw null; }
}
public abstract partial class CollectionResult<T> : System.ClientModel.ClientResult, System.Collections.Generic.IEnumerable<T>, System.Collections.IEnumerable
{
protected internal CollectionResult() { }
protected internal CollectionResult(System.ClientModel.Primitives.PipelineResponse response) { }
public abstract System.Collections.Generic.IEnumerator<T> GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
}
public partial class ContinuationToken
{
protected ContinuationToken() { }
protected ContinuationToken(System.BinaryData bytes) { }
public static System.ClientModel.ContinuationToken FromBytes(System.BinaryData bytes) { throw null; }
public virtual System.BinaryData ToBytes() { throw null; }
}
public abstract partial class PageCollection<T> : System.Collections.Generic.IEnumerable<System.ClientModel.PageResult<T>>, System.Collections.IEnumerable
{
protected PageCollection() { }
public System.Collections.Generic.IEnumerable<T> GetAllValues() { throw null; }
public System.ClientModel.PageResult<T> GetCurrentPage() { throw null; }
protected abstract System.ClientModel.PageResult<T> GetCurrentPageCore();
protected abstract System.Collections.Generic.IEnumerator<System.ClientModel.PageResult<T>> GetEnumeratorCore();
System.Collections.Generic.IEnumerator<System.ClientModel.PageResult<T>> System.Collections.Generic.IEnumerable<System.ClientModel.PageResult<T>>.GetEnumerator() { throw null; }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
}
public partial class PageResult<T> : System.ClientModel.ClientResult
{
internal PageResult() { }
public System.ClientModel.ContinuationToken? NextPageToken { get { throw null; } }
public System.ClientModel.ContinuationToken PageToken { get { throw null; } }
public System.Collections.Generic.IReadOnlyList<T> Values { get { throw null; } }
public static System.ClientModel.PageResult<T> Create(System.Collections.Generic.IReadOnlyList<T> values, System.ClientModel.ContinuationToken pageToken, System.ClientModel.ContinuationToken? nextPageToken, System.ClientModel.Primitives.PipelineResponse response) { throw null; }
}
}
API Usage
Example usage of AsyncCollectionResult<T>
with OpenAI chat completions operation:
AsyncCollectionResult<StreamingChatCompletionUpdate> updates
= client.CompleteChatStreamingAsync("Say 'this is a test.'");
Console.WriteLine($"[ASSISTANT]:");
await foreach (StreamingChatCompletionUpdate update in updates)
{
foreach (ChatMessageContentPart updatePart in update.ContentUpdate)
{
Console.Write(updatePart.Text);
}
}
Example usage of `AsyncPageCollection with OpenAI list assistants operation:
AsyncPageCollection<Assistant> assitantPages = client.GetAssistantsAsync();
IAsyncEnumerable<Assistant> assistants = assitantPages.GetAllValuesAsync();
await foreach (Assistant assistant in assistants)
{
Console.WriteLine($"[{count,3}] {assistant.Id} {assistant.CreatedAt:s} {assistant.Name}");
count++;
}
Sample usage of PageResult<T>
in ASP.NET web app "collection rehydration" scenario:
PageCollection<Assistant> assistantPages = (cachedPageTokenBytes is null) ?
// We don't have a token cached for the page of results to render.
// Request a new collection from user inputs.
assistantClient.GetAssistants(new AssistantCollectionOptions()
{
Order = listOrder,
PageSize = pageSize
}) :
// We have a serialized page token that was cached when a prior
// web app page was rendered.
// Rehydrate the page collection from the cached page token.
assistantClient.GetAssistants(ContinuationToken.FromBytes(cachedPageTokenBytes));
// Get the current page from the collection.
PageResult<Assistant> assitantPage = assistantPages.GetCurrentPage();
// Set the values for the web app page to render.
Assistants = assitantPage.Values;
// Cache the serialized page token value to use the next time
// the web app page is rendered.
CacheBytes("PageToken", assitantPage.PageToken.ToBytes());
A full implementation of a mock paging client can be found here.
Alternative Designs
No response
Risks
No response