-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
ClientModel: Add async and sync enumerable client results #43392
Conversation
namespace System.ClientModel; | ||
|
||
#pragma warning disable CS1591 // public XML comments | ||
public abstract class AsyncEnumerableResult<T> : ClientResult, IAsyncEnumerable<T>, IDisposable |
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.
Inheriting from ClientResult adds the GetRawResponse
method to this type. For enumerable client result implementations that use a polling mechanism and therefore have multiple responses associated with them, we might need to either say "GetRawResponse returns the first response and subtypes must expose other public APIs to access subsequent responses," or possibly expose a GetRawResponses method that returns a collection of PipelineResponse values. The latter is not a great Dev UX in the streaming response case where there is only a single response returned, but the former could require us to add additional types to support polling-enumerable scenarios such as paging.
|
||
#pragma warning disable CS1591 // public XML comments | ||
public abstract class AsyncEnumerableResult<T> : ClientResult, IAsyncEnumerable<T>, IDisposable | ||
where T : IPersistableModel<T> |
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.
Another question is whether or not we want to add this type constraint. The benefit is that it could simplify implementations by allowing them to use ModelReaderWriter to deserialize values coming across the wire, rather than requiring them to use other ways to pass in bespoke deserialization routines. A drawback is that it limits the use of the type to models that implement the interface -- if we wanted to enable scenarios like pages of primitives, that could be a problem.
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.
Since we did not constrain ClientResult<T>
, maybe we should keep not constraining. Though in retrospect, maybe we should have.
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 libraries haven't GA'ed yet, so we could still constrain it if we feel strongly about it.
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.
Ah, but not if we want Azure.Core Pageable<T> to be able to inherit from it. Alas! I will remove it.
sdk/core/System.ClientModel/src/Convenience/AsyncEnumerableResultOfT.cs
Outdated
Show resolved
Hide resolved
@@ -40,6 +48,14 @@ public partial class ClientResult<T> : System.ClientModel.ClientResult | |||
public virtual T Value { get { throw null; } } | |||
public static implicit operator T (System.ClientModel.ClientResult<T> result) { throw null; } | |||
} | |||
public abstract partial class EnumerableClientResult<T> : System.ClientModel.ClientResult, System.Collections.Generic.IEnumerable<T>, System.Collections.IEnumerable, System.IDisposable where T : System.ClientModel.Primitives.IPersistableModel<T> |
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 names don't seem symetrical. What about the following:
AsyncEnumerableResult
EnumerableResult
//or
ClientResults
ClientResultsAsync
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.
I'm actually not wild about ClientResults
because I think it complicates what we're defining a "result" to be (although I am open to debate here).
To take a concrete example, Pageable<T> is a collection of T's. Does the "result" mean the T or with the response? In a simple ClientResult<T> case there's a 1-1 relationship between them. But ClientResult itself just exposes GetRawResponse so it seems like result maps more closely to the response than the T. Given that, it feels strange to me to make the "result" plural when the T is the plural thing and there might only ever be one response.
Let me think.
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.
Looks like 3.5.2 in the FDG recommends using Collection
in the name of types that derive from IEnumerable<T>
... what about ClientResultCollection<T>
and AsyncClientResultCollection<T>
? Those are getting long, though, and don't really address my question about whether it is a collection of results, but I think I'm willing to drop argument since we defined ClientResult in the refdocs as "the result of a cloud service operation." A sequence of such results could easily be a result collection. Is it too long?
|
||
#pragma warning disable CS1591 // public XML comments | ||
public abstract class AsyncEnumerableResult<T> : ClientResult, IAsyncEnumerable<T>, IDisposable | ||
where T : IPersistableModel<T> |
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.
Since we did not constrain ClientResult<T>
, maybe we should keep not constraining. Though in retrospect, maybe we should have.
public abstract class AsyncEnumerableResult<T> : ClientResult, IAsyncEnumerable<T>, IAsyncDisposable | ||
where T : IPersistableModel<T> | ||
{ | ||
protected internal AsyncEnumerableResult(PipelineResponse response) : base(response) |
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.
can the response be updated/changed?
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.
I don't think it can. Is there a scenario where we would want that?
(Not sure I understand the implications of your question.)
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 scenario is using this type to expose pageable REST APIs. In such scenarios there are multiple responses representing all items in the collection/pageable
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.
Would you want to do that by changing the value returned from the response? Why not return a collection of PipelineResponses instead? (i.e. not inherit from ClientResult)
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.
Addressed. I see that we already do that in Azure.Core Operation implementations such as KeyVaultBackupOperation and FuzzySearchBatchOperation.
|
||
public abstract IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default); | ||
|
||
protected abstract ValueTask DisposeAsyncCore(); |
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.
FDGs say to make it virtual
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.
Removed per discussion about moving disposal of resources into the enumerator.
|
||
protected abstract ValueTask DisposeAsyncCore(); | ||
|
||
protected abstract void Dispose(bool disposing); |
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.
what is this for? i.e. who call it? BTW, FDGs (that Jeremy wrote) say to implement both IAsyncDisposabel and IDisposable
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.
Removed per discussion about moving disposal of resources into the enumerator.
…be replaced in a polling paradigm
API change check APIView has identified API level changes in this PR and created following API reviews. |
Closing in favor of #43840 |
Draft design proposal for discussion for
AsyncEnumerableResult<T>
andEnumerableClientResult<T>
types to add to System.ClientModel.Based on requirements from the OpenAI clients to support streaming endpoints for chat and assistants APIs, we have the need to be able to return a result from clients' service methods that represents a collection of values returned via the response's ContentStream.
In .NET, this is best modeled as an async stream, and this means such a ClientResult type in System.ClientModel should implement the IAsyncEnumerable interface. Since System.ClientModel clients also support synchronous service methods, we will provide a synchronous counterpart to the asynchronous that implements IEnumerable.
@tg-msft noted that these "streaming client results" look a lot like Azure.Core's
AsyncPageable<T>
andPageable<T>
types, and it is true that to the extent that are simply abstractions that represent collections of values, where streaming vs. polling is an implementation detail.There are discussion points in the comments of this PR noting that we could use a GetRawResponses method in place of a GetRawResponse method to support implementations that use multiple responses to return subsets of elements in the collection.
Addresses: