-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
[API Proposal]: Streaming APIs for the System.Net.Http.Json
extensions
#87577
Comments
Tagging subscribers to this area: @dotnet/ncl Issue DetailsBackground and motivationAs of .NET 8 preview 5, there are various extension methods within the
All of these APIs are Task-like returning, i.e.; either API Proposalnamespace System.Net.Http.Json;
public static partial class HttpClientJsonExtensions
{
static IAsyncEnumerable<TValue> GetFromJsonStreamAsync<TValue>(
this HttpClient client,
[StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri,
JsonSerializerOptions? options,
CancellationToken cancellationToken = default) { }
} API Usage// Imagine an ASP.NET Core Minimal API that returns an IAsyncEnumerable<T> where T is a `TimeSeries`.
public record class TimeSeries(string Name, DateTime dateTime, decimal Value);
// Imagine that this is the consuming code.
HttpClient client = new();
await foreach (var timeSeries in client.GetFromJsonStreamAsync<TimeSeries>("api/streaming/endpoint"))
{
// Use timeSeries values
} Alternative DesignsNo response RisksNo response
|
Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis Issue DetailsBackground and motivationAs of .NET 8 preview 5, there are various extension methods within the
All of these APIs are Task-like returning, i.e.; either API Proposalnamespace System.Net.Http.Json;
public static partial class HttpClientJsonExtensions
{
static IAsyncEnumerable<TValue> GetFromJsonStreamAsync<TValue>(
this HttpClient client,
[StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri,
JsonSerializerOptions? options,
CancellationToken cancellationToken = default) { }
} This would essentially encapsulate the following logic: public static class HttpRequestJsonExtensions
{
public static async IAsyncEnumerable<TValue?> GetFromJsonStreamAsync<TValue>(
this HttpClient client,
[StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri,
JsonSerializerOptions? options,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(client);
TimeSpan timeout = client.Timeout;
// Create the CTS before the initial SendAsync so that the SendAsync counts against the timeout.
CancellationTokenSource? linkedCTS = null;
if (timeout != Timeout.InfiniteTimeSpan)
{
linkedCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
linkedCTS.CancelAfter(timeout);
}
// We call SendAsync outside of the async Core method to propagate exception even without awaiting the returned task.
Task<HttpResponseMessage> responseTask;
try
{
// Intentionally using cancellationToken instead of the linked one here as HttpClient will enforce the Timeout on its own for this part
responseTask = client.GetAsync(CreateUri(requestUri), HttpCompletionOption.ResponseHeadersRead, cancellationToken);
}
catch
{
linkedCTS?.Dispose();
throw;
}
using HttpResponseMessage response = await responseTask.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
using Stream contentStream =
await response.Content
.ReadAsStreamAsync(cancellationToken)
.ConfigureAwait(false);
await foreach (TValue? value in JsonSerializer.DeserializeAsyncEnumerable<TValue>(contentStream, options, cancellationToken).ConfigureAwait(false))
{
yield return value;
}
}
private static Uri? CreateUri(string? uri) =>
string.IsNullOrEmpty(uri) ? null : new Uri(uri, UriKind.RelativeOrAbsolute);
} I'd love to be the one to implement this, if it's approved. API Usage// Imagine an ASP.NET Core Minimal API that returns an IAsyncEnumerable<T> where T is a `TimeSeries`.
public record class TimeSeries(string Name, DateTime dateTime, decimal Value);
// Imagine that this is the consuming code.
HttpClient client = new();
await foreach (var timeSeries in client.GetFromJsonStreamAsync<TimeSeries>("api/streaming/endpoint"))
{
// Use timeSeries values
} Alternative DesignsNo response RisksNo response
|
I think it's something we could consider for public static partial class HttpClientJsonExtensions
{
public static IAsyncEnumerable<TValue> GetFromJsonStreamAsync<TValue>(
this HttpClient client,
[StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri,
JsonSerializerOptions? options,
CancellationToken cancellationToken = default) { }
public static IAsyncEnumerable<TValue> GetFromJsonStreamAsync<TValue>(
this HttpClient client,
[StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri,
JsonTypeInfo<TValue> jsonTypeInfo,
CancellationToken cancellationToken = default) { }
} |
Per request by @eiriktsarpalis, here is a POC branch, and the commit with the proposed additions: main...IEvangelist:runtime:streaming-json-http-apis. |
Looks good overall, couple of remarks:
|
I've updated my POC branch, and the API proposal description with the suggested names. As for the |
Although I agree, that |
namespace System.Net.Http.Json;
public static partial class HttpClientJsonExtensions
{
public static IAsyncEnumerable<TValue?> GetFromJsonAsAsyncEnumerable<TValue>(
this HttpClient client,
[StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri,
JsonSerializerOptions? options,
CancellationToken cancellationToken = default);
public static IAsyncEnumerable<TValue?> GetFromJsonAsAsyncEnumerable<TValue>(
this HttpClient client,
Uri? requestUri,
JsonSerializerOptions? options,
CancellationToken cancellationToken = default);
public static IAsyncEnumerable<TValue?> GetFromJsonAsAsyncEnumerable<TValue>(
this HttpClient client,
[StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri,
JsonTypeInfo<TValue> jsonTypeInfo,
CancellationToken cancellationToken = default);
public static IAsyncEnumerable<TValue?> GetFromJsonAsAsyncEnumerable<TValue>(
this HttpClient client,
Uri? requestUri,
JsonTypeInfo<TValue> jsonTypeInfo,
CancellationToken cancellationToken = default);
public static IAsyncEnumerable<TValue?> GetFromJsonAsAsyncEnumerable<TValue>(
this HttpClient client,
[StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri,
CancellationToken cancellationToken = default);
public static IAsyncEnumerable<TValue?> GetFromJsonAsAsyncEnumerable<TValue>(
this HttpClient client,
Uri? requestUri,
CancellationToken cancellationToken = default);
}
public static partial class HttpContentJsonExtensions
{
public static IAsyncEnumerable<T?> ReadFromJsonAsAsyncEnumerable<T>(
this HttpContent content,
JsonSerializerOptions? options,
CancellationToken cancellationToken = default);
public static IAsyncEnumerable<T?> ReadFromJsonAsAsyncEnumerable<T>(
this HttpContent content,
CancellationToken cancellationToken = default);
public static IAsyncEnumerable<T?> ReadFromJsonAsAsyncEnumerable<T>(
this HttpContent content,
JsonTypeInfo<T> jsonTypeInfo,
CancellationToken cancellationToken = default);
} |
#89258) * Contributes to #87577 * More updates * Added unit tests, generated ref, and minor clean up * Added missing triple slash comments * Update src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com> * Correct the preprocessor directives, and delegate to JsonTypeInfo overload - per peer feedback. * Refactor for deferred execution, remove helper methods since they're no longer needed * Add test to ensure deferred execution semantics, updates from Miha. * Apply suggestions from code review Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com> * Update src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.AsyncEnumerable.cs Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com> * Update test per Miha's feedback * A few more updates from peer review, Eirik's nits. * No need for the length limit read stream. * Add limit per invocation, not element. Share length limit read stream logic. --------- Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com> Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
Background and motivation
As of .NET 8 preview 5 (and starting with .NET 5), there are various extension methods within the
System.Net.Http.Json
namespace (specifically theSystem.Net.Http.Json
NuGet package 📦) that expose JSON-centric functionality to conveniently encapsulate common workflows. Such as, but not limited to extending theHttpClient
:GetFromJsonAsync
PostAsJsonAsync
PutAsJsonAsync
PatchAsJsonAsync
DeleteFromJsonAsync
All of these APIs are Task-like returning, i.e., either
Task<TValue>
orValueTask<TValue>
However, there are no extension methods for some of the streaming APIs, that returnIAsyncEnumerable<TValue>
. While this is obviously possible without the use of in-built extension methods, it would be really convenient for the runtime to include said extensions.Ideally, we could encapsulate all of the logic required to efficiently delegate JSON-streaming calls to the
JsonSerializer.DeserializeAsyncEnumerable
functionality.API Proposal
This would be scoped to
GET
on theHttpClient
.And the
HttpContent
:Potential Implementation
This would essentially encapsulate the following logic:I'd love to be the one to implement this, if it's approved.
API Usage
Alternative Designs
No response
Risks
No response
The text was updated successfully, but these errors were encountered: