Skip to content
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: AsyncResultCollection<T> and SSE event collection implementation #43840

Merged
merged 53 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
54f822b
Add async and sync enumerable client results
annelo-msft Apr 12, 2024
d239e17
Implement IAsyncDisposable
annelo-msft Apr 15, 2024
115295a
Merge remote-tracking branch 'upstream/main' into clientmodel-asyncen…
annelo-msft Apr 19, 2024
3b12f1f
Remove constrant and disposable; update ClientResult so response can …
annelo-msft Apr 19, 2024
dbf18ad
rename and upate tests
annelo-msft Apr 20, 2024
a798fd9
Merge remote-tracking branch 'upstream/main' into clientmodel-asyncen…
annelo-msft May 1, 2024
72951b9
Merge remote-tracking branch 'upstream/main' into clientmodel-asyncen…
annelo-msft May 2, 2024
63e1351
initial addition of files from https://github.com/joseharriaga/openai…
annelo-msft May 2, 2024
f604598
make it build
annelo-msft May 2, 2024
d3bd6b8
hello world test
annelo-msft May 2, 2024
3fca028
bootstrap more tests
annelo-msft May 2, 2024
4ea146d
more internal tests
annelo-msft May 2, 2024
262c78d
adding enumerator tests; haven't figured out the batch piece yet
annelo-msft May 2, 2024
984499d
Make batch test pass
annelo-msft May 2, 2024
f26a89b
remove collection-event functionality and add tests for public type
annelo-msft May 3, 2024
647f1a9
reshuffle
annelo-msft May 3, 2024
3189fb3
Add mock convenience SSE type to give POC of lazy request sending
annelo-msft May 3, 2024
1a0c861
add tests of delayed request
annelo-msft May 3, 2024
d5cbc8e
Add BinaryData factory method
annelo-msft May 3, 2024
2d71562
Merge remote-tracking branch 'upstream/main' into clientmodel-sse-nod…
annelo-msft May 6, 2024
0beaeea
remove funcs for creating enumerators
annelo-msft May 6, 2024
fc84db1
renames
annelo-msft May 6, 2024
5a1bd13
postpone call to protocol method from convenience APIs
annelo-msft May 6, 2024
75e3e8a
implement IAsyncDisposable correctly
annelo-msft May 6, 2024
80e6ee5
initial pass over cancellation token
annelo-msft May 6, 2024
c27581d
Per FDG, throw OperationCanceledException if cancellation token is ca…
annelo-msft May 6, 2024
a04f543
remove factory method taking Func<T> and provide example of layering …
annelo-msft May 6, 2024
d6d4375
rename internal types and WIP adding reader tests
annelo-msft May 7, 2024
6fa96cb
nits
annelo-msft May 7, 2024
1fbd038
parameterize terminal event; TBD to provide virtual method on collect…
annelo-msft May 7, 2024
f577d2e
Merge remote-tracking branch 'upstream/main' into clientmodel-sse-nod…
annelo-msft May 7, 2024
f342162
WIP: nits
annelo-msft May 7, 2024
4ac87a2
WIP: added concatenation of data lines per SSE spec
annelo-msft May 8, 2024
dab0852
updates and bug fixes
annelo-msft May 8, 2024
1941f2c
add tests and update per SSE spec
annelo-msft May 8, 2024
dee2173
Merge remote-tracking branch 'upstream/main' into clientmodel-sse-nod…
annelo-msft May 8, 2024
58ac2a3
WIP: refactor to reuse field processing across sync and async methods
annelo-msft May 9, 2024
20a3b81
Merge remote-tracking branch 'upstream/main' into clientmodel-sse-nod…
annelo-msft May 9, 2024
88d5da1
make look a little more like the BCL type proposal
annelo-msft May 9, 2024
d2776d8
simplify field implementation a bit
annelo-msft May 9, 2024
2758377
cosmetic reworking of creating an event from a pending event
annelo-msft May 9, 2024
eae0caa
Remove factory method from public API; move MockSseClient to Tests.In…
annelo-msft May 9, 2024
b8f70f7
update API; reimplement mock client implementations without internal …
annelo-msft May 9, 2024
a592c2c
Add sync client result collection abstraction
annelo-msft May 9, 2024
88de121
tidy up and add tests
annelo-msft May 10, 2024
3bf665b
add default constructor to ClientResult
annelo-msft May 10, 2024
f87933e
more tidy-up
annelo-msft May 10, 2024
43dbbc2
rename and add refdocs
annelo-msft May 10, 2024
caebaea
comments
annelo-msft May 10, 2024
67392c1
Merge remote-tracking branch 'upstream/main' into clientmodel-sse-nod…
annelo-msft May 14, 2024
727185c
pr fb
annelo-msft May 14, 2024
df2df9e
rework last event id and retry per BCL design shift
annelo-msft May 14, 2024
af84592
add CHANGELOG entry
annelo-msft May 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk/core/Azure.Core.TestFramework/src/MockJsonModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Azure.Core.TestFramework
{
public class MockJsonModel : IJsonModel<MockJsonModel>
{
internal MockJsonModel()
public MockJsonModel()
{
}

Expand Down
17 changes: 16 additions & 1 deletion sdk/core/System.ClientModel/api/System.ClientModel.net6.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ public ApiKeyCredential(string key) { }
public static implicit operator System.ClientModel.ApiKeyCredential (string key) { throw null; }
public void Update(string key) { }
}
public abstract partial class AsyncResultCollection<T> : System.ClientModel.ClientResult, System.Collections.Generic.IAsyncEnumerable<T>
{
protected internal AsyncResultCollection() { }
protected internal AsyncResultCollection(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 BinaryContent : System.IDisposable
{
protected BinaryContent() { }
Expand All @@ -20,11 +26,13 @@ protected BinaryContent() { }
}
public partial class ClientResult
{
protected ClientResult() { }
protected ClientResult(System.ClientModel.Primitives.PipelineResponse response) { }
public static System.ClientModel.ClientResult<T?> FromOptionalValue<T>(T? value, System.ClientModel.Primitives.PipelineResponse response) { throw null; }
public static System.ClientModel.ClientResult FromResponse(System.ClientModel.Primitives.PipelineResponse response) { throw null; }
public static System.ClientModel.ClientResult<T> FromValue<T>(T value, System.ClientModel.Primitives.PipelineResponse response) { throw null; }
public System.ClientModel.Primitives.PipelineResponse GetRawResponse() { throw null; }
protected void SetRawResponse(System.ClientModel.Primitives.PipelineResponse response) { }
}
public partial class ClientResultException : System.Exception
{
Expand All @@ -36,10 +44,17 @@ public ClientResultException(string message, System.ClientModel.Primitives.Pipel
}
public partial class ClientResult<T> : System.ClientModel.ClientResult
{
protected internal ClientResult(T value, System.ClientModel.Primitives.PipelineResponse response) : base (default(System.ClientModel.Primitives.PipelineResponse)) { }
protected internal ClientResult(T value, System.ClientModel.Primitives.PipelineResponse response) { }
public virtual T Value { get { throw null; } }
public static implicit operator T (System.ClientModel.ClientResult<T> result) { throw null; }
}
public abstract partial class ResultCollection<T> : System.ClientModel.ClientResult, System.Collections.Generic.IEnumerable<T>, System.Collections.IEnumerable
{
protected internal ResultCollection() { }
protected internal ResultCollection(System.ClientModel.Primitives.PipelineResponse response) { }
public abstract System.Collections.Generic.IEnumerator<T> GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
}
}
namespace System.ClientModel.Primitives
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ public ApiKeyCredential(string key) { }
public static implicit operator System.ClientModel.ApiKeyCredential (string key) { throw null; }
public void Update(string key) { }
}
public abstract partial class AsyncResultCollection<T> : System.ClientModel.ClientResult, System.Collections.Generic.IAsyncEnumerable<T>
{
protected internal AsyncResultCollection() { }
protected internal AsyncResultCollection(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 BinaryContent : System.IDisposable
{
protected BinaryContent() { }
Expand All @@ -20,11 +26,13 @@ protected BinaryContent() { }
}
public partial class ClientResult
{
protected ClientResult() { }
protected ClientResult(System.ClientModel.Primitives.PipelineResponse response) { }
public static System.ClientModel.ClientResult<T?> FromOptionalValue<T>(T? value, System.ClientModel.Primitives.PipelineResponse response) { throw null; }
public static System.ClientModel.ClientResult FromResponse(System.ClientModel.Primitives.PipelineResponse response) { throw null; }
public static System.ClientModel.ClientResult<T> FromValue<T>(T value, System.ClientModel.Primitives.PipelineResponse response) { throw null; }
public System.ClientModel.Primitives.PipelineResponse GetRawResponse() { throw null; }
protected void SetRawResponse(System.ClientModel.Primitives.PipelineResponse response) { }
}
public partial class ClientResultException : System.Exception
{
Expand All @@ -36,10 +44,17 @@ public ClientResultException(string message, System.ClientModel.Primitives.Pipel
}
public partial class ClientResult<T> : System.ClientModel.ClientResult
{
protected internal ClientResult(T value, System.ClientModel.Primitives.PipelineResponse response) : base (default(System.ClientModel.Primitives.PipelineResponse)) { }
protected internal ClientResult(T value, System.ClientModel.Primitives.PipelineResponse response) { }
public virtual T Value { get { throw null; } }
public static implicit operator T (System.ClientModel.ClientResult<T> result) { throw null; }
}
public abstract partial class ResultCollection<T> : System.ClientModel.ClientResult, System.Collections.Generic.IEnumerable<T>, System.Collections.IEnumerable
{
protected internal ResultCollection() { }
protected internal ResultCollection(System.ClientModel.Primitives.PipelineResponse response) { }
public abstract System.Collections.Generic.IEnumerator<T> GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
}
}
namespace System.ClientModel.Primitives
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Threading;

namespace System.ClientModel;

/// <summary>
/// Represents a collection of results returned from a cloud service operation.
/// </summary>
public abstract class AsyncResultCollection<T> : ClientResult, IAsyncEnumerable<T>
{
/// <summary>
/// Create a new instance of <see cref="AsyncResultCollection{T}"/>.
/// </summary>
/// <remarks>If no <see cref="PipelineResponse"/> is provided when the
/// <see cref="ClientResult"/> instance is created, it is expected that
/// a derived type will call <see cref="ClientResult.SetRawResponse(PipelineResponse)"/>
/// prior to a user calling <see cref="ClientResult.GetRawResponse"/>.
/// This constructor is indended for use by collection implementations that
/// postpone sending a request until <see cref="GetAsyncEnumerator(CancellationToken)"/>
/// is called. Such implementations will typically be returned from client
/// convenience methods so that callers of the methods don't need to
/// dispose the return value. </remarks>
protected internal AsyncResultCollection() : base()
{
}

/// <summary>
/// Create a new instance of <see cref="AsyncResultCollection{T}"/>.
/// </summary>
/// <param name="response">The <see cref="PipelineResponse"/> holding the
/// items in the collection, or the first set of the items in the collection.
/// </param>
protected internal AsyncResultCollection(PipelineResponse response) : base(response)
{
}

/// <inheritdoc/>
public abstract IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
61 changes: 57 additions & 4 deletions sdk/core/System.ClientModel/src/Convenience/ClientResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,18 @@ namespace System.ClientModel;
/// </summary>
public class ClientResult
{
private readonly PipelineResponse _response;
private PipelineResponse? _response;

/// <summary>
/// Create a new instance of <see cref="ClientResult"/>.
/// </summary>
/// <remarks>If no <see cref="PipelineResponse"/> is provided when the
/// <see cref="ClientResult"/> instance is created, it is expected that
/// a derived type will call <see cref="SetRawResponse(PipelineResponse)"/>
/// prior to a user calling <see cref="GetRawResponse"/>.</remarks>
protected ClientResult()
{
}

/// <summary>
/// Create a new instance of <see cref="ClientResult"/> from a service
Expand All @@ -31,7 +42,39 @@ protected ClientResult(PipelineResponse response)
/// </summary>
/// <returns>the <see cref="PipelineResponse"/> received from the service.
/// </returns>
public PipelineResponse GetRawResponse() => _response;
/// <exception cref="InvalidOperationException">No
/// <see cref="PipelineResponse"/> value is currently available for this
/// <see cref="ClientResult"/> instance. This can happen when the instance
/// is a collection type like <see cref="AsyncResultCollection{T}"/>
/// that has not yet been enumerated.</exception>
public PipelineResponse GetRawResponse()
{
if (_response is null)
{
throw new InvalidOperationException("No response is associated " +
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
"with this result. If the result is a collection result " +
"type, this may be because no request has been sent to the " +
"server yet.");
}

return _response;
}

/// <summary>
/// Update the value returned from <see cref="GetRawResponse"/>.
/// </summary>
/// <remarks>This method may be called from types derived from
/// <see cref="ClientResult"/> that poll the service for status updates
/// or to retrieve additional collection values to update the raw response
/// to the response most recently returned from the service.</remarks>
/// <param name="response">The <see cref="PipelineResponse"/> to return
/// from <see cref="GetRawResponse"/>.</param>
protected void SetRawResponse(PipelineResponse response)
{
Argument.AssertNotNull(response, nameof(response));

_response = response;
}

#region Factory methods for ClientResult and subtypes

Expand All @@ -44,7 +87,11 @@ protected ClientResult(PipelineResponse response)
/// provided <paramref name="response"/>.
/// </returns>
public static ClientResult FromResponse(PipelineResponse response)
=> new ClientResult(response);
{
Argument.AssertNotNull(response, nameof(response));

return new ClientResult(response);
}

/// <summary>
/// Creates a new instance of <see cref="ClientResult{T}"/> that holds the
Expand All @@ -60,6 +107,8 @@ public static ClientResult FromResponse(PipelineResponse response)
/// </returns>
public static ClientResult<T> FromValue<T>(T value, PipelineResponse response)
{
Argument.AssertNotNull(response, nameof(response));

if (value is null)
{
string message = "ClientResult<T> contract guarantees that ClientResult<T>.Value is non-null. " +
Expand Down Expand Up @@ -90,7 +139,11 @@ public static ClientResult<T> FromValue<T>(T value, PipelineResponse response)
/// provided <paramref name="value"/> and <paramref name="response"/>.
/// </returns>
public static ClientResult<T?> FromOptionalValue<T>(T? value, PipelineResponse response)
=> new ClientResult<T?>(value, response);
{
Argument.AssertNotNull(response, nameof(response));

return new ClientResult<T?>(value, response);
}

#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.ClientModel.Primitives;
using System.Collections;
using System.Collections.Generic;

namespace System.ClientModel;

/// <summary>
/// Represents a collection of results returned from a cloud service operation.
/// </summary>
public abstract class ResultCollection<T> : ClientResult, IEnumerable<T>
{
/// <summary>
/// Create a new instance of <see cref="ResultCollection{T}"/>.
/// </summary>
/// <remarks>If no <see cref="PipelineResponse"/> is provided when the
/// <see cref="ClientResult"/> instance is created, it is expected that
/// a derived type will call <see cref="ClientResult.SetRawResponse(PipelineResponse)"/>
/// prior to a user calling <see cref="ClientResult.GetRawResponse"/>.
/// This constructor is indended for use by collection implementations that
/// postpone sending a request until <see cref="GetEnumerator()"/>
/// is called. Such implementations will typically be returned from client
/// convenience methods so that callers of the methods don't need to
/// dispose the return value. </remarks>
protected internal ResultCollection() : base()
{
}

/// <summary>
/// Create a new instance of <see cref="ResultCollection{T}"/>.
/// </summary>
/// <param name="response">The <see cref="PipelineResponse"/> holding the
/// items in the collection, or the first set of the items in the collection.
/// </param>
protected internal ResultCollection(PipelineResponse response) : base(response)
{
}

/// <inheritdoc/>
public abstract IEnumerator<T> GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace System.ClientModel.Internal;

/// <summary>
/// Represents a collection of SSE events that can be enumerated as a C# async stream.
/// </summary>
internal class AsyncServerSentEventEnumerable : IAsyncEnumerable<ServerSentEvent>
{
private readonly Stream _contentStream;

public AsyncServerSentEventEnumerable(Stream contentStream)
{
_contentStream = contentStream;
}

public IAsyncEnumerator<ServerSentEvent> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
return new AsyncServerSentEventEnumerator(_contentStream, cancellationToken);
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
}

private sealed class AsyncServerSentEventEnumerator : IAsyncEnumerator<ServerSentEvent>
{
private readonly CancellationToken _cancellationToken;
private readonly ServerSentEventReader _reader;

public ServerSentEvent Current { get; private set; }

public AsyncServerSentEventEnumerator(Stream contentStream, CancellationToken cancellationToken = default)
{
_reader = new(contentStream);
_cancellationToken = cancellationToken;
}

public async ValueTask<bool> MoveNextAsync()
{
ServerSentEvent? nextEvent = await _reader.TryGetNextEventAsync(_cancellationToken).ConfigureAwait(false);

if (nextEvent.HasValue)
{
Current = nextEvent.Value;
return true;
}

Current = default;
return false;
}

public ValueTask DisposeAsync()
{
// The creator of the enumerable has responsibility for disposing
// the content stream passed to the enumerable constructor.

#if NET6_0_OR_GREATER
return ValueTask.CompletedTask;
#else
return new ValueTask();
#endif
}
}
}
32 changes: 32 additions & 0 deletions sdk/core/System.ClientModel/src/Internal/SSE/ServerSentEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace System.ClientModel.Internal;

/// <summary>
/// Represents an SSE event.
/// See SSE specification: https://html.spec.whatwg.org/multipage/server-sent-events.html
/// </summary>
internal readonly struct ServerSentEvent
{
// Gets the value of the SSE "event type" buffer, used to distinguish between event kinds.
public string EventType { get; }

// Gets the value of the SSE "data" buffer, which holds the payload of the server-sent event.
public string Data { get; }

// Gets the value of the "last event ID" buffer, with which a user agent can reestablish a session.
public string? Id { get; }

// If present, gets the defined "retry" value for the event, which represents the delay before reconnecting.
public TimeSpan? ReconnectionTime { get; }
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved

public ServerSentEvent(string type, string data, string? id, string? retry)
{
EventType = type;
Data = data;
Id = id;
ReconnectionTime = retry is null ? null :
int.TryParse(retry, out int time) ? TimeSpan.FromMilliseconds(time) : null;
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading