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
93 changes: 93 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,99 @@ Apps targeting modern TFMs (such as .NET 5 and later) already include `System.Te

FluentHttpClient is available on [NuGet.org](https://www.nuget.org/packages/FluentHttpClient/) and can be installed using a NuGet package manager or the .NET CLI.

## When to Use FluentHttpClient

While `HttpClient` is a powerful and flexible tool, building HTTP requests with it often involves repetitive boilerplate, manual serialization, and scattered configuration logic. FluentHttpClient addresses these pain points by providing a fluent, chainable API that reduces cognitive load and improves code readability.

### Common HttpClient Challenges

**Repetitive Configuration**
Every request requires manually setting headers, query parameters, and content, often scattered across multiple lines. This makes it easy to miss required headers or forget encoding rules.

**Manual Serialization**
Converting objects to JSON, setting the correct `Content-Type`, and deserializing responses requires multiple steps and imports. Error-prone encoding and parsing logic often needs to be duplicated across your codebase.

**Inconsistent Error Handling**
Without a unified approach to handling success and failure responses, status code checks and logging logic tend to be duplicated or omitted entirely.

**Lifetime and Reuse Concerns**
Properly managing `HttpClient` lifetime, avoiding socket exhaustion, and reusing instances while still configuring per-request state requires careful planning and often leads to awkward patterns.

### How FluentHttpClient Helps

FluentHttpClient wraps `HttpClient` (you still manage the lifetime) and provides extension methods that let you configure requests in a single, readable chain:

- **Fluent Configuration**: Add headers, query parameters, cookies, and authentication in a natural, discoverable flow
- **Automatic Serialization**: Built-in JSON and XML serialization/deserialization with support for `System.Text.Json`, Native AOT, and custom options
- **Response Handlers**: Attach success and failure callbacks directly in the request chain without breaking fluency
- **Reduced Boilerplate**: Express the entire request lifecycle—configuration, sending, and deserialization—in a single expression

### Side-by-Side Comparison

Here's the same request implemented with raw `HttpClient` and FluentHttpClient:

#### Raw HttpClient

```csharp
using System.Net.Http.Json;

var client = new HttpClient
{
BaseAddress = new Uri("https://jsonplaceholder.typicode.com")
};

var request = new HttpRequestMessage(HttpMethod.Get, "/posts/1");
request.Headers.Add("X-Correlation-Id", correlationId);

var response = await client.SendAsync(request);

Post? post = null;
if (response.IsSuccessStatusCode)
{
post = await response.Content.ReadFromJsonAsync<Post>();
Console.WriteLine($"Success: {response.StatusCode}");
}
else
{
Console.WriteLine($"Failed: {response.StatusCode}");
}

public class Post
{
public int Id { get; set; }
public string? Title { get; set; }
public string? Body { get; set; }
}
```

#### FluentHttpClient

```csharp
using FluentHttpClient;

var client = new HttpClient
{
BaseAddress = new Uri("https://jsonplaceholder.typicode.com")
};

var post = await client
.UsingRoute("/posts/1")
.WithHeader("X-Correlation-Id", correlationId)
.GetAsync()
.OnSuccess(r => Console.WriteLine($"Success: {r.StatusCode}"))
.OnFailure(r => Console.WriteLine($"Failed: {r.StatusCode}"))
.ReadJsonAsync<Post>();

public class Post
{
public int Id { get; set; }
public string? Title { get; set; }
public string? Body { get; set; }
}
```

The FluentHttpClient version expresses the same logic in fewer lines, with better readability and no loss of functionality. All configuration, sending, error handling, and deserialization happen in a single fluent chain.

## Usage and Support

- Check out the project documentation https://scottoffen.github.io/fluenthttpclient.
Expand Down
80 changes: 72 additions & 8 deletions docs/docs/configure-cookies.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,42 @@ var builder = client

* Throws if `name` is `null`, empty, or whitespace.
* `null` values are converted to an empty string.
* Cookie values are **automatically URL-encoded** by default using RFC 6265 encoding.
* Existing cookies with the same name are overwritten.

### Controlling Encoding

By default, cookie values are URL-encoded to ensure special characters (such as `;`, `=`, `,`, and whitespace) do not break the Cookie header format. This is recommended for most use cases.

```csharp
// Default behavior - value is URL-encoded
var builder = client
.UsingBase()
.WithCookie("session", "value with spaces");

// Explicit encoding control
var builder = client
.UsingBase()
.WithCookie("session", "value with spaces", encode: true);

// Disable encoding (use with caution)
var preEncodedValue = Uri.EscapeDataString("my value");
var builder = client
.UsingBase()
.WithCookie("session", preEncodedValue, encode: false);
```

:::caution When to disable encoding

Set `encode` to `false` only if:
* The value is already properly encoded
* You need to preserve exact byte sequences for legacy systems
* You are certain the value contains no special characters

Disabling encoding with raw special characters can produce malformed Cookie headers.

:::

## Add Multiple Cookies

Use `WithCookies` to attach multiple cookies in one call.
Expand All @@ -39,26 +73,56 @@ var builder = client
* Throws if any cookie name is `null`, empty, or whitespace.
* Adds or overwrites existing entries.
* `null` values are stored as empty strings.
* Cookie values are **automatically URL-encoded** by default.

:::note
### Controlling Encoding for Multiple Cookies

Unlike query parameters or headers, cookies do not support multiple values per name.
The `encode` parameter applies to all cookies in the collection:

:::
```csharp
// Default behavior - all values are URL-encoded
var builder = client
.UsingBase()
.WithCookies(cookies);

// Explicit encoding control
var builder = client
.UsingBase()
.WithCookies(cookies, encode: true);

// Disable encoding for all cookies
var builder = client
.UsingBase()
.WithCookies(preEncodedCookies, encode: false);
```

## Behavior Notes

* Cookies are accumulated in `HttpRequestBuilder.Cookies`.
* Assigning the same cookie name overwrites the previous value.
* Cookies are applied to the final request using a standard `Cookie` header.
* Whitespace-only names are treated as invalid.
* Values are stored exactly as provided (aside from null becoming an empty string).
* **Values are URL-encoded by default** using `Uri.EscapeDataString` ([RFC 6265](https://datatracker.ietf.org/doc/html/rfc6265) compliance).
* When `encode` is `false`, values are stored exactly as provided (aside from `null` becoming an empty string).
* The encoding option defaults to `true` for safety and RFC compliance.
* Multiple cookies are assembled into a single `Cookie` header using semicolon separators.

:::note

Unlike query parameters or headers, cookies do not support multiple values per name.

:::

---

## Quick Reference

| Method | Purpose |
| ------------------------------------------------------- | ----------------------------------- |
| `WithCookie(string, string)` | Adds or overwrites a single cookie. |
| `WithCookies(IEnumerable<KeyValuePair<string,string>>)` | Add multiple cookies at once. |
| Method | Purpose |
| ------------------------------------------------------------- | ----------------------------------- |
| `WithCookie(string name, string value, bool encode = true)` | Adds or overwrites a single cookie. |
| `WithCookies(IEnumerable<KeyValuePair<string,string>>, bool encode = true)` | Add multiple cookies at once. |

**Parameters:**
* `name` - The cookie name (required, cannot be null/empty/whitespace)
* `value` - The cookie value (null becomes empty string)
* `encode` - Whether to URL-encode the value (default: `true`)
145 changes: 133 additions & 12 deletions docs/docs/configure-headers.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ sidebar_position: 2
title: Configure Headers
---

FluentHttpClient lets you configure request headers directly on `HttpRequestBuilder`. All header methods add *deferred configurators* to the builder, so headers are applied when the `HttpRequestMessage` is finally built, not when you call the fluent methods.
FluentHttpClient lets you configure request headers directly on `HttpRequestBuilder`. Header configuration uses two approaches:

* **String headers** (`WithHeader`, `WithHeaders`) are validated and stored immediately when called, providing fast fail-fast validation for common header scenarios.
* **Typed headers** (`WithAuthentication`, `WithBasicAuthentication`, `WithOAuthBearerToken`, `ConfigureHeaders`) use deferred configurators that are applied when the `HttpRequestMessage` is built, allowing strongly-typed header configuration with complex types like `CacheControl` and `Authorization`.

## Adding Single Headers

Expand Down Expand Up @@ -83,11 +86,110 @@ Use the bulk overloads when you already have headers in a collection (e.g. from

:::

## Typed Headers

For headers that require strongly-typed values (such as `Authorization`, `CacheControl`, `Accept`, `IfModifiedSince`, `IfNoneMatch`, and others), use `ConfigureHeaders` to configure them directly through the `HttpRequestHeaders` API.

### Using ConfigureHeaders

```csharp
var builder = client
.UsingBase()
.ConfigureHeaders(headers =>
{
headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
});
```

* Provides direct access to the `HttpRequestHeaders` collection.
* Supports all strongly-typed header properties available on `HttpRequestHeaders`.
* Throws if `configure` action is `null`.
* Configuration is applied when the request is built (deferred execution).

### Complex Headers

`ConfigureHeaders` is ideal for headers with multiple properties, quality values, or complex structures:

#### Cache Control

```csharp
builder.ConfigureHeaders(headers =>
{
headers.CacheControl = new CacheControlHeaderValue
{
NoCache = true,
NoStore = true,
MaxAge = TimeSpan.FromSeconds(30)
};
});
```

#### Accept with Quality Values

```csharp
builder.ConfigureHeaders(headers =>
{
headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml", 0.9));
headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain", 0.8));
});
```

#### Conditional Request Headers

```csharp
builder.ConfigureHeaders(headers =>
{
headers.IfModifiedSince = lastModified;
headers.IfNoneMatch.Add(new EntityTagHeaderValue("\"12345\""));
});
```

### Accumulation Behavior

Multiple calls to `ConfigureHeaders` accumulate - each configurator is stored and executed in order when the request is built:

```csharp
builder
.ConfigureHeaders(headers =>
headers.Authorization = new AuthenticationHeaderValue("Bearer", token))
.ConfigureHeaders(headers =>
headers.CacheControl = new CacheControlHeaderValue { NoCache = true })
.ConfigureHeaders(headers =>
headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")));

// All three configurators will be applied
```

If multiple configurators set the same header property, the last one wins (for single-value headers like `Authorization`). For collection-based headers (like `Accept`), all values accumulate unless a configurator explicitly clears the collection.

### When to Use ConfigureHeaders

Use `ConfigureHeaders` when you need:

* **Strongly-typed headers** like `Authorization`, `CacheControl`, `Accept`, `IfModifiedSince`
* **Headers with quality values** (e.g., `Accept: application/json;q=0.9`)
* **Headers with multiple properties** (e.g., `CacheControl` with NoCache, NoStore, MaxAge)
* **Collection-based headers** that support multiple values with typed entries
* **Direct access** to the `HttpRequestHeaders` API

For simple string-based headers (like `X-Correlation-Id` or `X-Tenant`), prefer `WithHeader` instead - it validates immediately, performs better, and keeps your code simpler.

### Performance Notes

`ConfigureHeaders` uses deferred execution, which means:

* Configurators are stored in a list and executed when the request is built.
* Multiple configurators have a small overhead compared to a single call.
* For high-throughput scenarios with simple headers, prefer `WithHeader`.

However, for most applications, the performance difference is negligible, and the strongly-typed API provides better compile-time safety and IntelliSense support.

## Reserved Headers

FluentHttpClient intentionally restricts a small set of HTTP headers that are controlled by the underlying `HttpClient` and its transport layers. These headers define wire-level framing and routing behavior, and overriding them can produce ambiguous requests, protocol violations, or security issues.

Because of this, the fluent header extensions do **not** allow setting the following headers:
Because of this, the fluent string-header extensions (`WithHeader` and `WithHeaders`) do **not** allow setting the following headers:

* `Host`
* `Content-Length`
Expand All @@ -97,9 +199,9 @@ These values are determined automatically based on the request URI, the configur

### Advanced Usage

This restriction only applies to the high-level fluent extensions. If advanced scenarios require manual control of these headers, you can still modify the underlying `HttpRequestMessage` using a configuration delegate (for example, via [`When`](./conditional-configuration.md) with an always-true bool or predicate). This opt-in approach allows experienced users to take full control without exposing casual users to common footguns.
This restriction only applies to the `WithHeader` and `Withheaders` fluent extensions. If advanced scenarios require manual control of these headers, you can still accomplish this using `ConfigureHeaders`. This opt-in approach allows experienced users to take full control without exposing casual users to common footguns.

In short, the fluent API keeps the safe path safe, while still leaving the door open for expert customization or tom-foolery when needed.
In short, the fluent API keeps the simple path safe, while still leaving the door open for expert customization or tom-foolery when needed.

:::tip Indirect Control

Expand Down Expand Up @@ -166,15 +268,33 @@ var builder = client

## Behavior Notes

All of these methods work by adding actions to `HttpRequestBuilder.HeaderConfigurators`:
### String Header Methods

`WithHeader` and `WithHeaders` methods:

* Store headers in an internal dictionary with **immediate validation**.
* Validate header keys and values when the method is called (fail-fast).
* Reject reserved headers (`Host`, `Content-Length`, `Transfer-Encoding`) immediately.
* Headers are case-insensitive (per HTTP specification).
* Multiple values for the same header key are supported and accumulated.
* Headers are applied to the `HttpRequestMessage` when the request is built.

### Typed Header Methods

`WithAuthentication`, `WithBasicAuthentication`, `WithOAuthBearerToken`, and `ConfigureHeaders`:

* Add deferred configurators to `HttpRequestBuilder.HeaderConfigurators`.
* Validation occurs when the request is built (when you call `SendAsync`).
* Multiple configurators are cumulative and applied in order.
* The last configurator that sets a particular header wins (e.g., `Authorization`).
* Configurators have direct access to strongly-typed header properties.

### General Behavior

* Headers are applied when the request is built (for example, when you call `SendAsync`).
* Multiple calls to header methods are cumulative:
* Multiple non-auth headers are combined as expected.
* The most recent method that sets `Authorization` wins.
* Headers are added via `TryAddWithoutValidation`, which:
* Skips strict header format checks.
* Still respects HTTP semantics at send time.
* All header configuration is cumulative - multiple calls add or update headers.
* String headers and typed headers can be mixed in the same request.
* Headers set via `WithHeader` take effect before typed header configurators run.
* Reserved headers (`Host`, `Content-Length`, `Transfer-Encoding`) cannot be set via string methods but may be set via advanced techniques (see Reserved Headers section).

---

Expand All @@ -186,6 +306,7 @@ All of these methods work by adding actions to `HttpRequestBuilder.HeaderConfigu
| `WithHeader(string key, IEnumerable<string> values)` | Add a header with multiple values. |
| `WithHeaders(IEnumerable<KeyValuePair<string, string>> headers)` | Add multiple headers, one value each. |
| `WithHeaders(IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers)` | Add multiple headers with multiple values each. |
| `ConfigureHeaders(Action<HttpRequestHeaders> configure)` | Configure strongly-typed headers directly. |
| `WithAuthentication(string scheme, string token)` | Set `Authorization` with a custom scheme. |
| `WithBasicAuthentication(string token)` | Set `Authorization: Basic {token}`. |
| `WithBasicAuthentication(string username, string password)` | Build and set a Basic auth token. |
Expand Down
Loading
Loading