Skip to content

Commit 5a79452

Browse files
authored
Performance, Documentation, and Standardization (#45)
* Update content extensions to use central method * Update class and method summary comments * Performance updates and documentation clarifications * Update unit tests * Update version * Update documentation * Add samples * Improve READMEs
1 parent 2fd236d commit 5a79452

55 files changed

Lines changed: 2044 additions & 568 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,99 @@ Apps targeting modern TFMs (such as .NET 5 and later) already include `System.Te
4040

4141
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.
4242

43+
## When to Use FluentHttpClient
44+
45+
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.
46+
47+
### Common HttpClient Challenges
48+
49+
**Repetitive Configuration**
50+
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.
51+
52+
**Manual Serialization**
53+
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.
54+
55+
**Inconsistent Error Handling**
56+
Without a unified approach to handling success and failure responses, status code checks and logging logic tend to be duplicated or omitted entirely.
57+
58+
**Lifetime and Reuse Concerns**
59+
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.
60+
61+
### How FluentHttpClient Helps
62+
63+
FluentHttpClient wraps `HttpClient` (you still manage the lifetime) and provides extension methods that let you configure requests in a single, readable chain:
64+
65+
- **Fluent Configuration**: Add headers, query parameters, cookies, and authentication in a natural, discoverable flow
66+
- **Automatic Serialization**: Built-in JSON and XML serialization/deserialization with support for `System.Text.Json`, Native AOT, and custom options
67+
- **Response Handlers**: Attach success and failure callbacks directly in the request chain without breaking fluency
68+
- **Reduced Boilerplate**: Express the entire request lifecycle—configuration, sending, and deserialization—in a single expression
69+
70+
### Side-by-Side Comparison
71+
72+
Here's the same request implemented with raw `HttpClient` and FluentHttpClient:
73+
74+
#### Raw HttpClient
75+
76+
```csharp
77+
using System.Net.Http.Json;
78+
79+
var client = new HttpClient
80+
{
81+
BaseAddress = new Uri("https://jsonplaceholder.typicode.com")
82+
};
83+
84+
var request = new HttpRequestMessage(HttpMethod.Get, "/posts/1");
85+
request.Headers.Add("X-Correlation-Id", correlationId);
86+
87+
var response = await client.SendAsync(request);
88+
89+
Post? post = null;
90+
if (response.IsSuccessStatusCode)
91+
{
92+
post = await response.Content.ReadFromJsonAsync<Post>();
93+
Console.WriteLine($"Success: {response.StatusCode}");
94+
}
95+
else
96+
{
97+
Console.WriteLine($"Failed: {response.StatusCode}");
98+
}
99+
100+
public class Post
101+
{
102+
public int Id { get; set; }
103+
public string? Title { get; set; }
104+
public string? Body { get; set; }
105+
}
106+
```
107+
108+
#### FluentHttpClient
109+
110+
```csharp
111+
using FluentHttpClient;
112+
113+
var client = new HttpClient
114+
{
115+
BaseAddress = new Uri("https://jsonplaceholder.typicode.com")
116+
};
117+
118+
var post = await client
119+
.UsingRoute("/posts/1")
120+
.WithHeader("X-Correlation-Id", correlationId)
121+
.GetAsync()
122+
.OnSuccess(r => Console.WriteLine($"Success: {r.StatusCode}"))
123+
.OnFailure(r => Console.WriteLine($"Failed: {r.StatusCode}"))
124+
.ReadJsonAsync<Post>();
125+
126+
public class Post
127+
{
128+
public int Id { get; set; }
129+
public string? Title { get; set; }
130+
public string? Body { get; set; }
131+
}
132+
```
133+
134+
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.
135+
43136
## Usage and Support
44137

45138
- Check out the project documentation https://scottoffen.github.io/fluenthttpclient.

docs/docs/configure-cookies.md

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,42 @@ var builder = client
1717

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

23+
### Controlling Encoding
24+
25+
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.
26+
27+
```csharp
28+
// Default behavior - value is URL-encoded
29+
var builder = client
30+
.UsingBase()
31+
.WithCookie("session", "value with spaces");
32+
33+
// Explicit encoding control
34+
var builder = client
35+
.UsingBase()
36+
.WithCookie("session", "value with spaces", encode: true);
37+
38+
// Disable encoding (use with caution)
39+
var preEncodedValue = Uri.EscapeDataString("my value");
40+
var builder = client
41+
.UsingBase()
42+
.WithCookie("session", preEncodedValue, encode: false);
43+
```
44+
45+
:::caution When to disable encoding
46+
47+
Set `encode` to `false` only if:
48+
* The value is already properly encoded
49+
* You need to preserve exact byte sequences for legacy systems
50+
* You are certain the value contains no special characters
51+
52+
Disabling encoding with raw special characters can produce malformed Cookie headers.
53+
54+
:::
55+
2256
## Add Multiple Cookies
2357

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

43-
:::note
78+
### Controlling Encoding for Multiple Cookies
4479

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

47-
:::
82+
```csharp
83+
// Default behavior - all values are URL-encoded
84+
var builder = client
85+
.UsingBase()
86+
.WithCookies(cookies);
87+
88+
// Explicit encoding control
89+
var builder = client
90+
.UsingBase()
91+
.WithCookies(cookies, encode: true);
92+
93+
// Disable encoding for all cookies
94+
var builder = client
95+
.UsingBase()
96+
.WithCookies(preEncodedCookies, encode: false);
97+
```
4898

4999
## Behavior Notes
50100

51101
* Cookies are accumulated in `HttpRequestBuilder.Cookies`.
52102
* Assigning the same cookie name overwrites the previous value.
53103
* Cookies are applied to the final request using a standard `Cookie` header.
54104
* Whitespace-only names are treated as invalid.
55-
* Values are stored exactly as provided (aside from null becoming an empty string).
105+
* **Values are URL-encoded by default** using `Uri.EscapeDataString` ([RFC 6265](https://datatracker.ietf.org/doc/html/rfc6265) compliance).
106+
* When `encode` is `false`, values are stored exactly as provided (aside from `null` becoming an empty string).
107+
* The encoding option defaults to `true` for safety and RFC compliance.
108+
* Multiple cookies are assembled into a single `Cookie` header using semicolon separators.
109+
110+
:::note
111+
112+
Unlike query parameters or headers, cookies do not support multiple values per name.
113+
114+
:::
56115

57116
---
58117

59118
## Quick Reference
60119

61-
| Method | Purpose |
62-
| ------------------------------------------------------- | ----------------------------------- |
63-
| `WithCookie(string, string)` | Adds or overwrites a single cookie. |
64-
| `WithCookies(IEnumerable<KeyValuePair<string,string>>)` | Add multiple cookies at once. |
120+
| Method | Purpose |
121+
| ------------------------------------------------------------- | ----------------------------------- |
122+
| `WithCookie(string name, string value, bool encode = true)` | Adds or overwrites a single cookie. |
123+
| `WithCookies(IEnumerable<KeyValuePair<string,string>>, bool encode = true)` | Add multiple cookies at once. |
124+
125+
**Parameters:**
126+
* `name` - The cookie name (required, cannot be null/empty/whitespace)
127+
* `value` - The cookie value (null becomes empty string)
128+
* `encode` - Whether to URL-encode the value (default: `true`)

docs/docs/configure-headers.md

Lines changed: 133 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ sidebar_position: 2
33
title: Configure Headers
44
---
55

6-
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.
6+
FluentHttpClient lets you configure request headers directly on `HttpRequestBuilder`. Header configuration uses two approaches:
7+
8+
* **String headers** (`WithHeader`, `WithHeaders`) are validated and stored immediately when called, providing fast fail-fast validation for common header scenarios.
9+
* **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`.
710

811
## Adding Single Headers
912

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

8487
:::
8588

89+
## Typed Headers
90+
91+
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.
92+
93+
### Using ConfigureHeaders
94+
95+
```csharp
96+
var builder = client
97+
.UsingBase()
98+
.ConfigureHeaders(headers =>
99+
{
100+
headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
101+
});
102+
```
103+
104+
* Provides direct access to the `HttpRequestHeaders` collection.
105+
* Supports all strongly-typed header properties available on `HttpRequestHeaders`.
106+
* Throws if `configure` action is `null`.
107+
* Configuration is applied when the request is built (deferred execution).
108+
109+
### Complex Headers
110+
111+
`ConfigureHeaders` is ideal for headers with multiple properties, quality values, or complex structures:
112+
113+
#### Cache Control
114+
115+
```csharp
116+
builder.ConfigureHeaders(headers =>
117+
{
118+
headers.CacheControl = new CacheControlHeaderValue
119+
{
120+
NoCache = true,
121+
NoStore = true,
122+
MaxAge = TimeSpan.FromSeconds(30)
123+
};
124+
});
125+
```
126+
127+
#### Accept with Quality Values
128+
129+
```csharp
130+
builder.ConfigureHeaders(headers =>
131+
{
132+
headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
133+
headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml", 0.9));
134+
headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain", 0.8));
135+
});
136+
```
137+
138+
#### Conditional Request Headers
139+
140+
```csharp
141+
builder.ConfigureHeaders(headers =>
142+
{
143+
headers.IfModifiedSince = lastModified;
144+
headers.IfNoneMatch.Add(new EntityTagHeaderValue("\"12345\""));
145+
});
146+
```
147+
148+
### Accumulation Behavior
149+
150+
Multiple calls to `ConfigureHeaders` accumulate - each configurator is stored and executed in order when the request is built:
151+
152+
```csharp
153+
builder
154+
.ConfigureHeaders(headers =>
155+
headers.Authorization = new AuthenticationHeaderValue("Bearer", token))
156+
.ConfigureHeaders(headers =>
157+
headers.CacheControl = new CacheControlHeaderValue { NoCache = true })
158+
.ConfigureHeaders(headers =>
159+
headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")));
160+
161+
// All three configurators will be applied
162+
```
163+
164+
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.
165+
166+
### When to Use ConfigureHeaders
167+
168+
Use `ConfigureHeaders` when you need:
169+
170+
* **Strongly-typed headers** like `Authorization`, `CacheControl`, `Accept`, `IfModifiedSince`
171+
* **Headers with quality values** (e.g., `Accept: application/json;q=0.9`)
172+
* **Headers with multiple properties** (e.g., `CacheControl` with NoCache, NoStore, MaxAge)
173+
* **Collection-based headers** that support multiple values with typed entries
174+
* **Direct access** to the `HttpRequestHeaders` API
175+
176+
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.
177+
178+
### Performance Notes
179+
180+
`ConfigureHeaders` uses deferred execution, which means:
181+
182+
* Configurators are stored in a list and executed when the request is built.
183+
* Multiple configurators have a small overhead compared to a single call.
184+
* For high-throughput scenarios with simple headers, prefer `WithHeader`.
185+
186+
However, for most applications, the performance difference is negligible, and the strongly-typed API provides better compile-time safety and IntelliSense support.
187+
86188
## Reserved Headers
87189

88190
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.
89191

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

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

98200
### Advanced Usage
99201

100-
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.
202+
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.
101203

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

104206
:::tip Indirect Control
105207

@@ -166,15 +268,33 @@ var builder = client
166268

167269
## Behavior Notes
168270

169-
All of these methods work by adding actions to `HttpRequestBuilder.HeaderConfigurators`:
271+
### String Header Methods
272+
273+
`WithHeader` and `WithHeaders` methods:
274+
275+
* Store headers in an internal dictionary with **immediate validation**.
276+
* Validate header keys and values when the method is called (fail-fast).
277+
* Reject reserved headers (`Host`, `Content-Length`, `Transfer-Encoding`) immediately.
278+
* Headers are case-insensitive (per HTTP specification).
279+
* Multiple values for the same header key are supported and accumulated.
280+
* Headers are applied to the `HttpRequestMessage` when the request is built.
281+
282+
### Typed Header Methods
283+
284+
`WithAuthentication`, `WithBasicAuthentication`, `WithOAuthBearerToken`, and `ConfigureHeaders`:
285+
286+
* Add deferred configurators to `HttpRequestBuilder.HeaderConfigurators`.
287+
* Validation occurs when the request is built (when you call `SendAsync`).
288+
* Multiple configurators are cumulative and applied in order.
289+
* The last configurator that sets a particular header wins (e.g., `Authorization`).
290+
* Configurators have direct access to strongly-typed header properties.
291+
292+
### General Behavior
170293

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

179299
---
180300

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

0 commit comments

Comments
 (0)