[API Proposal]: Add QUERY verb HttpClient JSON extension methods
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Background and motivation
[The new QUERY verb](https://datatracker.ietf.org/doc/html/rfc10008) is intended to facilitate both request *and* response bodies for the same transaction. That puts us in a bit of an awkward position with the extension methods.
Until now, the verbs fell roughly into two camps: read operations that expected response bodies to be parsed from JSON (the `FromJson` methods for `GET` and `DELETE`), and write operations that expected request bodies to be serialized as JSON (the `AsJson` methods for `PATCH`, `POST`, and `PUT`). The existing extension methods reflect that split. Though `POST`/`PATCH`/`PUT` responses can often return bodies, it was defensible not to include extensions facilitating that, since those verbs are primarily a mechanism for submitting data. However, the most common scenario for `QUERY` is that both the request and the response contain bodies that need to be serialized and deserialized.
These extensions depend on `QUERY` support being added to `HttpClient` itself, which is proposed in [dotnet/runtime#113522](https://github.com/dotnet/runtime/issues/113522).
These are the possible extension variants I can imagine adding:
- Accept serializable `TValue`s as request bodies and return `HttpResponseMessage`s. These are the `AsJson` methods.
- Accept `HttpContent`s and return `TValue`s deserialized from response bodies. These are the `FromJson` methods.
- Accept serializable `TRequestValue`s and return deserialized `TResponseValue`s. **These are a new class of methods with no naming convention yet.**
- Accept `HttpContent`s and return `IAsyncEnumerable`s deserialized from response bodies. These are the `FromJsonAsAsyncEnumerable` methods.
- Accept serializable `TRequestValue`s and return deserialized `IAsyncEnumerable`s. **These are a new class of methods with no naming convention yet.**
The best name I've been able to come up with for the "JSON both ways" methods is `WithJson`, but I'm looking for other suggestions.
### API Proposal
```csharp
namespace System.Net.Http.Json
{
public static class HttpClientJsonExtensions
{
// These extensions follow the existing extension patterns with minimal changes.
// AsJson
public static Task QueryAsJsonAsync(this HttpClient client, string? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default);
public static Task QueryAsJsonAsync(this HttpClient client, Uri? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default);
public static Task QueryAsJsonAsync(this HttpClient client, string? requestUri, TValue value, CancellationToken cancellationToken);
public static Task QueryAsJsonAsync(this HttpClient client, Uri? requestUri, TValue value, CancellationToken cancellationToken);
public static Task QueryAsJsonAsync(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken = default);
public static Task QueryAsJsonAsync(this HttpClient client, Uri? requestUri, TValue value, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken = default);
// FromJson
public static Task QueryFromJsonAsync(this HttpClient client, string? requestUri, HttpContent? content, Type type, CancellationToken cancellationToken = default);
public static Task QueryFromJsonAsync(this HttpClient client, Uri? requestUri, HttpContent? content, Type type, CancellationToken cancellationToken = default);
public static Task QueryFromJsonAsync(this HttpClient client, string? requestUri, HttpContent? content, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken = default);
public static Task QueryFromJsonAsync(this HttpClient client, Uri? requestUri, HttpContent? content, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken = default);
public static Task QueryFromJsonAsync(this HttpClient client, string? requestUri, HttpContent? content, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default);
public static Task QueryFromJsonAsync(this HttpClient client, Uri? requestUri, HttpContent? content, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default);
public static Task QueryFromJsonAsync(this HttpClient client, string? requestUri, HttpContent? content, CancellationToken cancellationToken = default);
public static Task QueryFromJsonAsync(this HttpClient client, Uri? requestUri, HttpContent? content, CancellationToken cancellationToken = default);
public static Task QueryFromJsonAsync(this HttpClient client, string? requestUri, HttpContent? content, JsonSerializerOptions? options, CancellationToken cancellationToken = default);
public static Task QueryFromJsonAsync(this HttpClient client, Uri? requestUri, HttpContent? content, JsonSerializerOptions? options, CancellationToken cancellationToken = default);
public static Task QueryFromJsonAsync(this HttpClient client, string? requestUri, HttpContent? content, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken = default);
public static Task QueryFromJsonAsync(this HttpClient client, Uri? requestUri, HttpContent? content, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken = default);
// FromJsonAsAsyncEnumerable
public static IAsyncEnumerable QueryFromJsonAsAsyncEnumerable(this HttpClient client, string? requestUri, HttpContent? content, CancellationToken cancellationToken = default);
public static IAsyncEnumerable QueryFromJsonAsAsyncEnumerable(this HttpClient client, Uri? requestUri, HttpContent? content, CancellationToken cancellationToken = default);
public static IAsyncEnumerable QueryFromJsonAsAsyncEnumerable(this HttpClient client, string? requestUri, HttpContent? content, JsonSerializerOptions? options, CancellationToken cancellationToken = default);
public static IAsyncEnumerable QueryFromJsonAsAsyncEnumerable(this HttpClient client, Uri? requestUri, HttpContent? content, JsonSerializerOptions? options, CancellationToken cancellationToken = default);
public static IAsyncEnumerable QueryFromJsonAsAsyncEnumerable(this HttpClient client, string? requestUri, HttpContent? content, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken = default);
public static IAsyncEnumerable QueryFromJsonAsAsyncEnumerable(this HttpClient client, Uri? requestUri, HttpContent? content, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken = default);
// These are the new "class" of methods. They still try to track the existing methods as closely as possible.
// WithJson?
public static Task QueryWithJsonAsync(this HttpClient client, string? requestUri, TRequestValue value, Type responseType, CancellationToken cancellationToken = default);
public static Task QueryWithJsonAsync(this HttpClient client, Uri? requestUri, TRequestValue value, Type responseType, CancellationToken cancellationToken = default);
public static Task QueryWithJsonAsync(this HttpClient client, string? requestUri, TRequestValue value, Type responseType, JsonSerializerOptions? options, CancellationToken cancellationToken = default);
public static Task QueryWithJsonAsync(this HttpClient client, Uri? requestUri, TRequestValue value, Type responseType, JsonSerializerOptions? options, CancellationToken cancellationToken = default);
public static Task QueryWithJsonAsync(this HttpClient client, string? requestUri, TRequestValue value, Type responseType, JsonSerializerContext context, CancellationToken cancellationToken = default);
public static Task QueryWithJsonAsync(this HttpClient client, Uri? requestUri, TRequestValue value, Type responseType, JsonSerializerContext context, CancellationToken cancellationToken = default);
public static Task QueryWithJsonAsync(this HttpClient client, string? requestUri, TRequestValue value, CancellationToken cancellationToken = default);
public static Task QueryWithJsonAsync(this HttpClient client, Uri? requestUri, TRequestValue value, CancellationToken cancellationToken = default);
public static Task QueryWithJsonAsync(this HttpClient client, string? requestUri, TRequestValue value, JsonSerializerOptions? options, CancellationToken cancellationToken = default);
public static Task QueryWithJsonAsync(this HttpClient client, Uri? requestUri, TRequestValue value, JsonSerializerOptions? options, CancellationToken cancellationToken = default);
public static Task QueryWithJsonAsync(this HttpClient client, string? requestUri, TRequestValue value, JsonTypeInfo jsonRequestTypeInfo, JsonTypeInfo jsonResponseTypeInfo, CancellationToken cancellationToken = default);
public static Task QueryWithJsonAsync(this HttpClient client, Uri? requestUri, TRequestValue value, JsonTypeInfo jsonRequestTypeInfo, JsonTypeInfo jsonResponseTypeInfo, CancellationToken cancellationToken = default);
// WithJsonAsAsyncEnumerable?
public static IAsyncEnumerable QueryWithJsonAsAsyncEnumerable(this HttpClient client, string? requestUri, TRequestValue value, CancellationToken cancellationToken = default);
public static IAsyncEnumerable QueryWithJsonAsAsyncEnumerable(this HttpClient client, Uri? requestUri, TRequestValue value, CancellationToken cancellationToken = default);
public static IAsyncEnumerable QueryWithJsonAsAsyncEnumerable(this HttpClient client, string? requestUri, TRequestValue value, JsonSerializerOptions? options, CancellationToken cancellationToken = default);
public static IAsyncEnumerable QueryWithJsonAsAsyncEnumerable(this HttpClient client, Uri? requestUri, TRequestValue value, JsonSerializerOptions? options, CancellationToken cancellationToken = default);
public static IAsyncEnumerable QueryWithJsonAsAsyncEnumerable(this HttpClient client, string? requestUri, TRequestValue value, JsonTypeInfo jsonRequestTypeInfo, JsonTypeInfo jsonResponseTypeInfo, CancellationToken cancellationToken = default);
public static IAsyncEnumerable QueryWithJsonAsAsyncEnumerable(this HttpClient client, Uri? requestUri, TRequestValue value, JsonTypeInfo jsonRequestTypeInfo, JsonTypeInfo jsonResponseTypeInfo, CancellationToken cancellationToken = default);
}
}
```
### API Usage
```csharp
HttpClient httpClient = new();
StringContent requestContent = new("field eq 'value'", Encoding.UTF8, "application/query+text");
MyDeserializedBody? responseBodyObject = await httpClient.QueryFromJsonAsync("https://myuri.com/path", requestContent);
```
```csharp
HttpClient httpClient = new();
MySerializedRequestBody requestBodyObject = new();
HttpResponseMessage response = await httpClient.QueryAsJsonAsync("https://myuri.com/path", requestBodyObject);
```
```csharp
HttpClient httpClient = new();
MySerializedRequestBody requestBodyObject = new();
MyDeserializedBody? responseBodyObject = await httpClient.QueryWithJsonAsync("https://myuri.com/path", requestBodyObject);
```
```csharp
HttpClient httpClient = new();
MySerializedRequestBody requestBodyObject = new();
MyDeserializedBody? responseBodyObject = await httpClient.QueryWithJsonAsync(
"https://myuri.com/path",
requestBodyObject,
MyJsonContext.Default.MySerializedRequestBody,
MyJsonContext.Default.MyDeserializedBody);
```
```csharp
HttpClient httpClient = new();
MySerializedRequestBody requestBodyObject = new();
await foreach (MyDeserializedBody? item in httpClient.QueryWithJsonAsAsyncEnumerable("https://myuri.com/path", requestBodyObject))
{
Console.WriteLine(item);
}
```
### Alternative Designs
_No response_
### Risks
- The `HttpContent` parameters are nullable. There aren't really any good reasons to send a `QUERY` without a body, but the standard defines a body as "expected" in the same way it is expected for a `POST`, rather than as strictly mandatory, and the proposed `QUERY` methods on `HttpClient` also allow null content.
- The `Type` parameters are named `type` on the `FromJson` overloads (matching the existing extensions) and `responseType` on the `WithJson` overloads, where a request type is also in play. I think that's defensible, but it's worth more thought.
- The `WithJson` overloads with two type arguments can't infer `TResponseValue` from their arguments, so callers have to specify both type arguments explicitly on every overload except the `JsonTypeInfo` ones. Something like [this](https://github.com/dotnet/csharplang/discussions/92) might make this better in the future.
Contributor guide
Research direction
Start by reviewing the proposed HttpClientJsonExtensions signatures and the dependency on dotnet/runtime#113522 for HttpClient QUERY support. Compare the proposed methods with existing JSON extension patterns and follow the comment discussion about naming. Done means the request/response and async-enumerable API variants, names, and signatures have a resolved design.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- api
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100