dotnet / dotnet/runtime

[API Proposal]: Add QUERY verb HttpClient JSON extension methods

Open
#132,092 4 comments 0 reactions 0 assignees View on GitHub
api-suggestion area-System.Net.Http
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.