From e5369a379cd47111c8551a6aecacab6622fd4500 Mon Sep 17 00:00:00 2001 From: Scott Offen <3513626+scottoffen@users.noreply.github.com> Date: Wed, 5 Aug 2026 07:38:02 -0600 Subject: [PATCH 1/2] Update conditional guard --- src/FluentHttpClient/FluentJsonSerializer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FluentHttpClient/FluentJsonSerializer.cs b/src/FluentHttpClient/FluentJsonSerializer.cs index 0778d1c..94e062b 100644 --- a/src/FluentHttpClient/FluentJsonSerializer.cs +++ b/src/FluentHttpClient/FluentJsonSerializer.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; -#if NETSTANDARD2_1_OR_GREATER +#if !NETSTANDARD2_0 using System.Text.Json.Serialization; #endif @@ -17,7 +17,7 @@ internal static class FluentJsonSerializer public static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, -#if NETSTANDARD2_1_OR_GREATER +#if !NETSTANDARD2_0 NumberHandling = JsonNumberHandling.AllowReadingFromString, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull #endif From 15ec9f1b14d4150b8c7296d76f6c300004228447 Mon Sep 17 00:00:00 2001 From: Scott Offen <3513626+scottoffen@users.noreply.github.com> Date: Wed, 5 Aug 2026 07:52:54 -0600 Subject: [PATCH 2/2] Add support for QUERY HTTP verb and update documentation --- README.md | 3 + docs/docs/index.md | 6 ++ docs/docs/sending-requests.md | 18 +++++ .../FluentSendExtensionsTests.cs | 57 +++++++++++++++ src/FluentHttpClient/FluentSendExtensions.cs | 69 +++++++++++++++++++ src/FluentHttpClient/README.md | 2 + src/version.json | 2 +- 7 files changed, 156 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ee2c6f..8836b41 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ It works with the `HttpClient` you already have rather than replacing it. Each r - **Response handlers** that attach success and failure callbacks inline, without interrupting the chain. - **Extensible by subclassing**: derive from `HttpRequestBuilder` to create a custom builder shaped for a specific API or concern. Your methods chain alongside the built-in ones, and an override of `SendAsync` applies your logic to every request, since every other member on the class feeds into it. +> [!NOTE] +> FluentHttpClient has always included the ability to use custom HTTP verbs when sending requests. As of 5.1.0, we've added a dedicated `QueryAsync` method family, mirroring `GetAsync`, `PostAsync`, and the rest with the same four overloads, for the QUERY verb defined in [RFC 10008](https://datatracker.ietf.org/doc/html/rfc10008). + ## Side-by-Side The same request, written with raw `HttpClient` and with FluentHttpClient. Both deserialize the response into the same model: diff --git a/docs/docs/index.md b/docs/docs/index.md index 0c905a3..2cefa91 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -42,6 +42,12 @@ FluentHttpClient is built around the way you actually write HTTP code: configure - **Deserialize** - Handle responses with extensions for reading content (string, bytes, stream) and strongly-typed JSON/XML deserialization, so the last step in your chain gives you the shape you actually care about. +:::note + +FluentHttpClient has always included the ability to use custom HTTP verbs when sending requests. As of 5.1.0, we've added a dedicated `QueryAsync` method family, mirroring `GetAsync`, `PostAsync`, and the rest with the same four overloads, for the QUERY verb defined in [RFC 10008](https://datatracker.ietf.org/doc/html/rfc10008). + +::: + ```csharp var httpClient = new HttpClient(); diff --git a/docs/docs/sending-requests.md b/docs/docs/sending-requests.md index 5a21f12..8ec5ad3 100644 --- a/docs/docs/sending-requests.md +++ b/docs/docs/sending-requests.md @@ -30,6 +30,23 @@ var response = await builder.GetAsync(); * `Task GetAsync(HttpCompletionOption completionOption)` * `Task GetAsync(HttpCompletionOption completionOption, CancellationToken cancellationToken)` +### QUERY + +Use QUERY to send a request body describing a query, while keeping the safe and idempotent semantics of GET. It fills the gap between GET, which cannot carry a request body, and POST, which is neither safe nor idempotent. See [RFC 10008](https://datatracker.ietf.org/doc/html/rfc10008) for details. + +```csharp +var response = await builder + .WithJsonContent(searchCriteria) + .QueryAsync(); +``` + +**Available overloads** + +* `Task QueryAsync()` +* `Task QueryAsync(CancellationToken cancellationToken)` +* `Task QueryAsync(HttpCompletionOption completionOption)` +* `Task QueryAsync(HttpCompletionOption completionOption, CancellationToken cancellationToken)` + ### POST Use POST for creating resources or sending commands, typically with a request body. @@ -230,6 +247,7 @@ var response = await builder | Method group | Overloads | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `GetAsync` | `GetAsync()`, `GetAsync(CancellationToken)`, `GetAsync(HttpCompletionOption)`, `GetAsync(HttpCompletionOption, CancellationToken)` | +| `QueryAsync` | `QueryAsync()`, `QueryAsync(CancellationToken)`, `QueryAsync(HttpCompletionOption)`, `QueryAsync(HttpCompletionOption, CancellationToken)` | | `PostAsync` | `PostAsync()`, `PostAsync(CancellationToken)`, `PostAsync(HttpCompletionOption)`, `PostAsync(HttpCompletionOption, CancellationToken)` | | `PutAsync` | `PutAsync()`, `PutAsync(CancellationToken)`, `PutAsync(HttpCompletionOption)`, `PutAsync(HttpCompletionOption, CancellationToken)` | | `DeleteAsync` | `DeleteAsync()`, `DeleteAsync(CancellationToken)`, `DeleteAsync(HttpCompletionOption)`, `DeleteAsync(HttpCompletionOption, CancellationToken)` | diff --git a/src/FluentHttpClient.Tests/FluentSendExtensionsTests.cs b/src/FluentHttpClient.Tests/FluentSendExtensionsTests.cs index 6cdf9f7..238b9b5 100644 --- a/src/FluentHttpClient.Tests/FluentSendExtensionsTests.cs +++ b/src/FluentHttpClient.Tests/FluentSendExtensionsTests.cs @@ -429,4 +429,61 @@ public async Task PutAsync_UsesPutMethod_WhenCompletionOptionAndCancellationToke handler.LastRequest!.Method.ShouldBe(HttpMethod.Put); } } + + public class QueryAsyncTests + { + [Fact] + public async Task QueryAsync_UsesQueryMethod_WhenCalledWithoutParameters() + { + var handler = new TestHttpMessageHandler(); + var builder = CreateBuilder(handler); + + var response = await builder.QueryAsync(); + + response.ShouldNotBeNull(); + handler.LastRequest.ShouldNotBeNull(); + handler.LastRequest!.Method.ShouldBe(HttpMethod.Query); + } + + [Fact] + public async Task QueryAsync_UsesQueryMethod_WhenCancellationTokenProvided() + { + var handler = new TestHttpMessageHandler(); + var builder = CreateBuilder(handler); + using var cts = new CancellationTokenSource(); + + var response = await builder.QueryAsync(cts.Token); + + response.ShouldNotBeNull(); + handler.LastRequest.ShouldNotBeNull(); + handler.LastRequest!.Method.ShouldBe(HttpMethod.Query); + } + + [Fact] + public async Task QueryAsync_UsesQueryMethod_WhenCompletionOptionProvided() + { + var handler = new TestHttpMessageHandler(); + var builder = CreateBuilder(handler); + + var response = await builder.QueryAsync(HttpCompletionOption.ResponseHeadersRead); + + response.ShouldNotBeNull(); + handler.LastRequest.ShouldNotBeNull(); + handler.LastRequest!.Method.ShouldBe(HttpMethod.Query); + } + + [Fact] + public async Task QueryAsync_UsesQueryMethod_WhenCompletionOptionAndCancellationTokenProvided() + { + var handler = new TestHttpMessageHandler(); + var builder = CreateBuilder(handler); + using var cts = new CancellationTokenSource(); + + var response = await builder.QueryAsync(HttpCompletionOption.ResponseContentRead, cts.Token); + + response.ShouldNotBeNull(); + handler.LastRequest.ShouldNotBeNull(); + handler.LastRequest!.Method.ShouldBe(HttpMethod.Query); + } + } } diff --git a/src/FluentHttpClient/FluentSendExtensions.cs b/src/FluentHttpClient/FluentSendExtensions.cs index 0fb252d..134613c 100644 --- a/src/FluentHttpClient/FluentSendExtensions.cs +++ b/src/FluentHttpClient/FluentSendExtensions.cs @@ -392,4 +392,73 @@ public static Task PutAsync( { return builder.SendAsync(HttpMethod.Put, completionOption, cancellationToken); } + + // QUERY + + /// + /// Sends an HTTP QUERY request using the configured . + /// + /// The instance. + /// A task that represents the asynchronous operation. The task result contains the HTTP response message. + public static Task QueryAsync(this HttpRequestBuilder builder) + { +#if NET10_0_OR_GREATER + return builder.SendAsync(HttpMethod.Query); +#else + return builder.SendAsync("QUERY"); +#endif + } + + /// + /// Sends an HTTP QUERY request using the specified . + /// + /// The instance. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains the HTTP response message. + public static Task QueryAsync( + this HttpRequestBuilder builder, + CancellationToken cancellationToken) + { +#if NET10_0_OR_GREATER + return builder.SendAsync(HttpMethod.Query, cancellationToken: cancellationToken); +#else + return builder.SendAsync("QUERY", cancellationToken: cancellationToken); +#endif + } + + /// + /// Sends an HTTP QUERY request using the specified . + /// + /// The instance. + /// Indicates when the operation should complete. + /// A task that represents the asynchronous operation. The task result contains the HTTP response message. + public static Task QueryAsync( + this HttpRequestBuilder builder, + HttpCompletionOption completionOption) + { +#if NET10_0_OR_GREATER + return builder.SendAsync(HttpMethod.Query, completionOption); +#else + return builder.SendAsync("QUERY", completionOption); +#endif + } + + /// + /// Sends an HTTP QUERY request using the specified and . + /// + /// The instance. + /// Indicates when the operation should complete. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains the HTTP response message. + public static Task QueryAsync( + this HttpRequestBuilder builder, + HttpCompletionOption completionOption, + CancellationToken cancellationToken) + { +#if NET10_0_OR_GREATER + return builder.SendAsync(HttpMethod.Query, completionOption, cancellationToken); +#else + return builder.SendAsync("QUERY", completionOption, cancellationToken); +#endif + } } diff --git a/src/FluentHttpClient/README.md b/src/FluentHttpClient/README.md index 350593b..12a7919 100644 --- a/src/FluentHttpClient/README.md +++ b/src/FluentHttpClient/README.md @@ -12,6 +12,8 @@ It works with the `HttpClient` you already have rather than replacing it. Each r - **Response handlers** that attach success and failure callbacks inline, without interrupting the chain. - **Extensible by subclassing**: derive from `HttpRequestBuilder` to create a custom builder shaped for a specific API or concern. Your methods chain alongside the built-in ones, and an override of `SendAsync` applies your logic to every request, since every other member on the class feeds into it. +> **Note:** FluentHttpClient has always included the ability to use custom HTTP verbs when sending requests. As of 5.1.0, we've added a dedicated `QueryAsync` method family, mirroring `GetAsync`, `PostAsync`, and the rest with the same four overloads, for the QUERY verb defined in [RFC 10008](https://datatracker.ietf.org/doc/html/rfc10008). + ## Side-by-Side The same request, written with raw `HttpClient` and with FluentHttpClient. Both deserialize the response into the same model: diff --git a/src/version.json b/src/version.json index 377d25a..d4b84cc 100644 --- a/src/version.json +++ b/src/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "5.0", + "version": "5.1.0", "publicReleaseRefSpec": [ "^refs/heads/main$", "^refs/heads/v\\d+(?:\\.\\d+)?$"