Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
18 changes: 18 additions & 0 deletions docs/docs/sending-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,23 @@ var response = await builder.GetAsync();
* `Task<HttpResponseMessage> GetAsync(HttpCompletionOption completionOption)`
* `Task<HttpResponseMessage> 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<HttpResponseMessage> QueryAsync()`
* `Task<HttpResponseMessage> QueryAsync(CancellationToken cancellationToken)`
* `Task<HttpResponseMessage> QueryAsync(HttpCompletionOption completionOption)`
* `Task<HttpResponseMessage> QueryAsync(HttpCompletionOption completionOption, CancellationToken cancellationToken)`

### POST

Use POST for creating resources or sending commands, typically with a request body.
Expand Down Expand Up @@ -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)` |
Expand Down
57 changes: 57 additions & 0 deletions src/FluentHttpClient.Tests/FluentSendExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
4 changes: 2 additions & 2 deletions src/FluentHttpClient/FluentJsonSerializer.cs
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down
69 changes: 69 additions & 0 deletions src/FluentHttpClient/FluentSendExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -392,4 +392,73 @@ public static Task<HttpResponseMessage> PutAsync(
{
return builder.SendAsync(HttpMethod.Put, completionOption, cancellationToken);
}

// QUERY

/// <summary>
/// Sends an HTTP QUERY request using the configured <see cref="HttpRequestBuilder"/>.
/// </summary>
/// <param name="builder">The <see cref="HttpRequestBuilder"/> instance.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the HTTP response message.</returns>
public static Task<HttpResponseMessage> QueryAsync(this HttpRequestBuilder builder)
{
#if NET10_0_OR_GREATER
return builder.SendAsync(HttpMethod.Query);
#else
return builder.SendAsync("QUERY");
#endif
}

/// <summary>
/// Sends an HTTP QUERY request using the specified <see cref="CancellationToken"/>.
/// </summary>
/// <param name="builder">The <see cref="HttpRequestBuilder"/> instance.</param>
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the HTTP response message.</returns>
public static Task<HttpResponseMessage> 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
}

/// <summary>
/// Sends an HTTP QUERY request using the specified <see cref="HttpCompletionOption"/>.
/// </summary>
/// <param name="builder">The <see cref="HttpRequestBuilder"/> instance.</param>
/// <param name="completionOption">Indicates when the operation should complete.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the HTTP response message.</returns>
public static Task<HttpResponseMessage> QueryAsync(
this HttpRequestBuilder builder,
HttpCompletionOption completionOption)
{
#if NET10_0_OR_GREATER
return builder.SendAsync(HttpMethod.Query, completionOption);
#else
return builder.SendAsync("QUERY", completionOption);
#endif
}

/// <summary>
/// Sends an HTTP QUERY request using the specified <see cref="HttpCompletionOption"/> and <see cref="CancellationToken"/>.
/// </summary>
/// <param name="builder">The <see cref="HttpRequestBuilder"/> instance.</param>
/// <param name="completionOption">Indicates when the operation should complete.</param>
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the HTTP response message.</returns>
public static Task<HttpResponseMessage> 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
}
}
2 changes: 2 additions & 0 deletions src/FluentHttpClient/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/version.json
Original file line number Diff line number Diff line change
@@ -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+)?$"
Expand Down