Open IEvangelist opened 2 months ago
Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis See info in area-owners.md if you want to be subscribed.
There's no precedent in us using two generic parameters for the request and response types, the existing POST methods-system-threading-cancellationtoken)) simply return an HttpResponseMessage
. I wasn't involved in that design, but I'm guessing it was done to keep the APIs simple.
If I'm honest, I think the existing HttpContent.ReadFromJsonAsAsyncEnumerable
methods are the ideally suited workaround which work for any request type:
RequestModel model = ...;
HttpResponseMessage response = await client.PostAsJsonAsync("/openai/chat", model);
await foreach (ResponseModel item in response.ReadFromJsonAsAsyncEnumerable<ResponseModel>())
{
...
}
On the other hand, that sample demonstrates how easy it is to use it wrong.
The PostAsJsonAsync
call will buffer the whole response, meaning you won't actually get streaming semantics.
This would preserve the goal of streaming the response, but also with the difference that you now don't enforce the timeout/response size limits.
var request = new HttpRequestMessage(HttpMethod.Post, "/openai/chat")
{
Content = JsonContent.Create(model)
};
using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
await foreach (ResponseModel item in response.ReadFromJsonAsAsyncEnumerable<ResponseModel>())
{
...
}
cc: @dotnet/ncl
Does this help? It's my shot at it, and it appears to not buffer and stream.
public static async IAsyncEnumerable<TResponse> PostFromJsonAsAsyncEnumerable<TRequest, TResponse>(
this HttpClient client,
string requestUri,
TRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri);
using var ms = new MemoryStream();
await JsonSerializer.SerializeAsync(ms, request, JsonSerializerSettings.Options, cancellationToken: cancellationToken);
ms.Position = 0;
using var requestContent = new StreamContent(ms);
requestMessage.Content = requestContent;
requestContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
requestMessage.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
var items = response.Content.ReadFromJsonAsAsyncEnumerable<TResponse>(cancellationToken);
await foreach (var item in items)
{
if (cancellationToken.IsCancellationRequested)
throw new TaskCanceledException();
if (item is null)
continue;
yield return item;
}
}
The
PostAsJsonAsync
call will buffer the whole response, meaning you won't actually get streaming semantics.
Perhaps then the answer is to expose overloads to the existing HttpClientJsonExtensions
methods accepting HttpCompletionOptions
?
I'd like to have support for this.
There's no precedent in us using two generic parameters for the request and response types
for one generic parameter, maybe we could add API for
public IAsyncEnumerable<TResponse> PostFromJsonAsAsyncEnumerable<TResponse>(this HttpClient client, HttpContent? content, CancellationToken cancellationToken = default)
possible implementation
public async IAsyncEnumerable<TResponse> PostFromJsonAsAsyncEnumerable<TResponse>(this HttpClient client, HttpContent? content, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
using var response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await foreach (var item in response.Content
.ReadFromJsonAsAsyncEnumerable<TResponse>(cancellationToken).ConfigureAwait(false))
yield return item;
}
For two generic parameters, seemed there's a related issue (almost missed https://github.com/dotnet/runtime/issues/34157
Background and motivation
As a follow up to the related API proposal #87577, this aims to include
Post*
APIs to theHttpClientJsonExtensions
class as extensions methods.API Proposal
API Usage
Alternative Designs
No response
Risks
No response