Skip to content

Commit 4669acb

Browse files
committed
Update documentation
1 parent c5f34ea commit 4669acb

5 files changed

Lines changed: 338 additions & 129 deletions

File tree

README.md

Lines changed: 36 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,71 +3,36 @@
33
[![docs](https://img.shields.io/badge/docs-github.io-blue)](https://scottoffen.github.io/fluenthttpclient)
44
[![NuGet](https://img.shields.io/nuget/v/fluenthttpclient)](https://www.nuget.org/packages/FluentHttpClient/)
55
[![MIT](https://img.shields.io/github/license/scottoffen/fluenthttpclient?color=blue)](./LICENSE)
6-
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-blue.svg)](code_of_conduct.md)
6+
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-blue.svg)](CODE_OF_CONDUCT.md)
77
[![FluentHttpClient](https://img.shields.io/badge/FluentHttpClient-strong%20named-ff8038.svg)](https://learn.microsoft.com/dotnet/standard/assembly/strong-named)
8-
[![Multi-targeted](https://img.shields.io/badge/TFMs-multi--targeted-652f94)](#compatibility-matrix)
8+
[![Multi-targeted](https://img.shields.io/badge/TFMs-multi--targeted-652f94)](#target-frameworks)
99

10-
FluentHttpClient brings a modern, chainable API to [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient), turning verbose request setup into clean, expressive fluency. It handles headers, options, cookies, query parameters, conditional configurators, buffering, and *both* JSON/XML serialization and deserialization, along with success and failure handlers, all with minimal ceremony. It multitargets from **.NET Standard 2.0** all the way up through **.NET 10**, giving you broad compatibility across older runtimes and the latest platforms, with full Native AOT compatibility and strong-named assemblies.
10+
FluentHttpClient adds a chainable API on top of [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient). You configure a request, send it, and read the response in one expression, instead of building an `HttpRequestMessage`, checking the status code, and deserializing by hand every time.
1111

12-
## Compatibility Matrix
12+
It works with the `HttpClient` you already have rather than replacing it. Each request is built on its own without changing the client or its shared configuration, so your existing setup, including `IHttpClientFactory` and typed clients, is unaffected.
1313

14-
FluentHttpClient is optimized for .NET 10 and the newest .NET releases, while also supporting older platforms through .NET Standard 2.1 and 2.0 for teams maintaining long-lived or legacy applications. It includes full Native AOT compatibility and provides strong-named assemblies for environments that require them.
14+
## What You Get
1515

16-
| Target | Supported | Notes |
17-
| ------------------------- | --------- | ----------------------------- |
18-
| **.NET Standard 2.0** | ✔️ | Broadest compatibility target |
19-
| **.NET Standard 2.1** | ✔️ | Improved modern API surface |
20-
| **.NET Framework 4.6.1+** | ✔️ | Via `netstandard2.0` |
21-
| **.NET 6** | ✔️ | LTS |
22-
| **.NET 7** | ✔️ | |
23-
| **.NET 8** | ✔️ | LTS |
24-
| **.NET 9** | ✔️ | |
25-
| **.NET 10** | ✔️ | LTS |
16+
- **Fluent configuration** of headers, query parameters, cookies, authentication, options, content, and content buffering, all in one readable chain.
17+
- **JSON and XML** serialization and deserialization, with `JsonTypeInfo<T>` overloads for trim-safe and Native AOT scenarios.
18+
- **Conditional configuration** that applies immediately or defers until the request is built, so you can branch without breaking the chain.
19+
- **Response handlers** that attach success and failure callbacks inline, without interrupting the chain.
20+
- **Extensible by subclassing**: derive from `HttpRequestBuilder` for a thin client that keeps the full fluent API, then add your own methods or override behavior on top. Your additions chain alongside the built-in methods, and an override of `SendAsync` applies shared behavior such as authentication or logging to every request the client sends.
2621

27-
### .NETStandard Consumers
22+
## Side-by-Side
2823

29-
Projects targeting **.NETStandard 2.0** or **.NETStandard 2.1** do not include `System.Text.Json` in the framework. FluentHttpClient uses `System.Text.Json` internally for its JSON extensions, but the package is not referenced transitively.
24+
The same request, written with raw `HttpClient` and with FluentHttpClient. Both deserialize the response into the same model:
3025

31-
If you are building against **netstandard2.0** or **netstandard2.1**, or any TFM that does **not** ship `System.Text.Json`, you will need to add an explicit package reference, with a minimum version of 4.6.0 or 6.0.10, respectively. A higher version is always recommended.
32-
33-
Apps targeting modern TFMs (such as .NET 5 and later) already include `System.Text.Json` and do not require this step.
34-
35-
## Installation
36-
37-
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.
38-
39-
## When to Use FluentHttpClient
40-
41-
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.
42-
43-
### Common HttpClient Challenges
44-
45-
**Repetitive Configuration**
46-
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.
47-
48-
**Manual Serialization**
49-
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.
50-
51-
**Inconsistent Error Handling**
52-
Without a unified approach to handling success and failure responses, status code checks and logging logic tend to be duplicated or omitted entirely.
53-
54-
**Lifetime and Reuse Concerns**
55-
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.
56-
57-
### How FluentHttpClient Helps
58-
59-
FluentHttpClient wraps `HttpClient` (you still manage the lifetime) and provides extension methods that let you configure requests in a single, readable chain:
60-
61-
- **Fluent Configuration**: Add headers, query parameters, cookies, and authentication in a natural, discoverable flow
62-
- **Automatic Serialization**: Built-in JSON and XML serialization/deserialization with support for `System.Text.Json`, Native AOT, and custom options
63-
- **Response Handlers**: Attach success and failure callbacks directly in the request chain without breaking fluency
64-
- **Reduced Boilerplate**: Express the entire request lifecycle—configuration, sending, and deserialization—in a single expression
65-
66-
### Side-by-Side Comparison
67-
68-
Here's the same request implemented with raw `HttpClient` and FluentHttpClient:
26+
```csharp
27+
public class Post
28+
{
29+
public int Id { get; set; }
30+
public string? Title { get; set; }
31+
public string? Body { get; set; }
32+
}
33+
```
6934

70-
#### Raw HttpClient
35+
### Raw HttpClient
7136

7237
```csharp
7338
using System.Net.Http.Json;
@@ -92,16 +57,9 @@ else
9257
{
9358
Console.WriteLine($"Failed: {response.StatusCode}");
9459
}
95-
96-
public class Post
97-
{
98-
public int Id { get; set; }
99-
public string? Title { get; set; }
100-
public string? Body { get; set; }
101-
}
10260
```
10361

104-
#### FluentHttpClient
62+
### FluentHttpClient
10563

10664
```csharp
10765
using FluentHttpClient;
@@ -118,20 +76,23 @@ var post = await client
11876
.OnSuccess(r => Console.WriteLine($"Success: {r.StatusCode}"))
11977
.OnFailure(r => Console.WriteLine($"Failed: {r.StatusCode}"))
12078
.ReadJsonAsync<Post>();
121-
122-
public class Post
123-
{
124-
public int Id { get; set; }
125-
public string? Title { get; set; }
126-
public string? Body { get; set; }
127-
}
12879
```
12980

130-
Because a fluent API improves developer experience by turning tedious, repetitive setup into a readable, chainable flow that matches how you actually think about building and sending an HTTP request, 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.
81+
The FluentHttpClient version expresses the same logic in fewer lines, and keeps configuration, sending, error handling, and deserialization together in one chain.
82+
83+
## Target Frameworks
84+
85+
FluentHttpClient multitargets .NET Standard 2.0 and 2.1, and .NET 6, 7, 8, 9, and 10. Through .NET Standard 2.0 it also runs on .NET Framework 4.6.1 and later. The assemblies are strong-named, and the package is Native AOT compatible when you use the `JsonTypeInfo<T>` JSON overloads.
86+
87+
### .NET Standard Consumers
88+
89+
.NET Standard 2.0 and 2.1 do not ship `System.Text.Json`, and FluentHttpClient does not bring it in transitively. If you target either one, or any other framework that does not include `System.Text.Json`, add an explicit package reference: at least `4.6.0` for `netstandard2.0` or `6.0.10` for `netstandard2.1`. A newer version is always preferable. Apps on .NET 5 and later already include it and need no extra step.
90+
91+
## Documentation
13192

132-
## Usage and Support
93+
Full documentation, including how to build your own client types, is at https://scottoffen.github.io/fluenthttpclient.
13394

134-
- Check out the project documentation https://scottoffen.github.io/fluenthttpclient.
95+
## Community and Support
13596

13697
- Engage in our [community discussions](https://github.com/scottoffen/fluenthttpclient/discussions) for Q&A, ideas, and show and tell!
13798

@@ -162,4 +123,4 @@ FluentHttpClient is licensed under the [MIT](./LICENSE) license.
162123

163124
## Using FluentHttpClient? We'd Love To Hear About It!
164125

165-
Few thing are as satisfying as hearing that your open source project is being used and appreciated by others. Jump over to the discussion boards and [share the love](https://github.com/scottoffen/fluenthttpclient/discussions)!
126+
Few things are as satisfying as hearing that your open source project is being used and appreciated by others. Jump over to the discussion boards and [share the love](https://github.com/scottoffen/fluenthttpclient/discussions)!

docs/docs/purpose-built-clients.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
---
2+
sidebar_position: 16
3+
title: Purpose-Built Clients
4+
---
5+
6+
A purpose-built client is a thin class of your own that derives from `HttpRequestBuilder`. Because it is a builder, every FluentHttpClient extension already works on it, so callers get the same fluent experience they would from any builder. What you add on top is a single place to change behavior or expose your own methods.
7+
8+
This page covers the inheritance approach. The more common approach, a plain class that holds an `HttpClient` and forwards to it, is the standard wrapper pattern and is covered elsewhere in these docs. Reach for inheritance when you want the builder's own fluent surface plus a place to override or extend it.
9+
10+
## Why Subclass HttpRequestBuilder
11+
12+
Subclassing gives you three things in one small class.
13+
14+
You keep the full fluent API. Your client is an `HttpRequestBuilder`, so `WithHeader`, `WithQueryParameter`, `GetAsync`, the response handlers, and the deserialization extensions all work on it without any forwarding code.
15+
16+
You get one place to change behavior. Override `SendAsync` and your logic runs on every request the client sends, which is a natural home for an auth header, a correlation id, or logging.
17+
18+
You can add your own methods, and they chain. The fluent extensions return your subtype, not the base `HttpRequestBuilder`, so a helper you add stays reachable in the middle of a chain alongside the built-in methods.
19+
20+
## Lifetime
21+
22+
This is the part to settle before you wire anything up.
23+
24+
`HttpRequestBuilder` is the fluent front for a single `HttpRequestMessage`. It accumulates the state of one request, the route, headers, query string, and content, and is meant to be used once and thrown away. A subclass inherits that nature, so a purpose-built client is a per-request object.
25+
26+
:::important Per request, not per application
27+
A purpose-built client is short-lived, like the `HttpRequestMessage` it builds, not long-lived like `HttpClient`. Create one, send one request, discard it. Do not cache an instance and reuse it across requests, because its accumulated state would carry over.
28+
:::
29+
30+
The `HttpClient` underneath stays long-lived. That is the point of the factory approach below: the client is created fresh per request, while the `HttpClient` it wraps comes from `IHttpClientFactory`, which pools and rotates the expensive handler. You end up with two layered lifetimes, a long-lived transport and a short-lived request builder on top of it.
31+
32+
## A Purpose-Built Client
33+
34+
Here is a small client for the GitHub API. It exposes route selection, adds one fluent helper, and overrides `SendAsync` to apply a header to every request.
35+
36+
```csharp
37+
using FluentHttpClient;
38+
39+
public class GitHubClient : HttpRequestBuilder
40+
{
41+
public GitHubClient(HttpClient client) : base(client)
42+
{
43+
}
44+
45+
// Route selection exposed on the client, the way HttpClient.UsingRoute works.
46+
public GitHubClient UsingRoute(string route)
47+
{
48+
SetRoute(route);
49+
return this;
50+
}
51+
52+
// Added functionality: a fluent helper that returns the subtype, so it keeps chaining.
53+
public GitHubClient WithApiVersion(string version)
54+
{
55+
InternalHeaders["X-GitHub-Api-Version"] = new[] { version };
56+
return this;
57+
}
58+
59+
// Overridden behavior: runs on every request this client sends.
60+
public override Task<HttpResponseMessage> SendAsync(
61+
HttpMethod method,
62+
HttpCompletionOption completionOption,
63+
CancellationToken cancellationToken)
64+
{
65+
InternalHeaders["Accept"] = new[] { "application/vnd.github+json" };
66+
return base.SendAsync(method, completionOption, cancellationToken);
67+
}
68+
}
69+
```
70+
71+
A few things worth calling out.
72+
73+
The constructor forwards to the base. The base constructors are `protected internal`, which is what lets you chain to them with `base(...)` from your own assembly. `UsingRoute` sets the route through the protected `SetRoute`, which a subclass can call to set or change the route after construction. `SetRoute` is `protected`, so it stays out of the standard fluent surface; your client decides whether to expose route selection at all, and how. Because the route no longer has to be fixed at construction, the factory below can hand you a client without knowing the route up front.
74+
75+
You override one method, not many. `SendAsync(HttpMethod, HttpCompletionOption, CancellationToken)` is the only virtual overload. Every other send method, including `GetAsync` and the string-based overloads, routes through it, so this one override covers them all.
76+
77+
`UsingRoute` and `WithApiVersion` both return `GitHubClient`. Because the built-in extensions also return your subtype, you can mix your own methods into a chain and the type is preserved the whole way:
78+
79+
```csharp
80+
var user = await client
81+
.UsingRoute("/users/octocat")
82+
.WithApiVersion("2022-11-28")
83+
.WithHeader("X-Trace-Id", traceId)
84+
.GetAsync()
85+
.ReadJsonAsync<User>();
86+
```
87+
88+
## Creating Instances
89+
90+
There are two ways to hand these out through dependency injection. They differ in lifetime and wiring. In both, the route is chosen on the client with `UsingRoute`.
91+
92+
### With a Factory (recommended)
93+
94+
A factory is the better fit for a per-request client. The factory itself is long-lived, and each call returns a fresh client. The factory does not need to know the route. The caller chooses it on the client with `UsingRoute`, so `Create` takes nothing.
95+
96+
```csharp
97+
public class GitHubClientFactory
98+
{
99+
private readonly IHttpClientFactory _httpClientFactory;
100+
101+
public GitHubClientFactory(IHttpClientFactory httpClientFactory)
102+
{
103+
_httpClientFactory = httpClientFactory;
104+
}
105+
106+
public GitHubClient Create() => new(_httpClientFactory.CreateClient("github"));
107+
}
108+
```
109+
110+
Register the named `HttpClient` for its base address and shared configuration, then register the factory as a singleton.
111+
112+
```csharp
113+
services.AddHttpClient("github", client =>
114+
{
115+
client.BaseAddress = new Uri("https://api.github.com/");
116+
client.DefaultRequestHeaders.UserAgent.ParseAdd("my-app");
117+
});
118+
119+
services.AddSingleton<GitHubClientFactory>();
120+
```
121+
122+
Inject the factory and create a client per request.
123+
124+
```csharp
125+
public class ProfileService
126+
{
127+
private readonly GitHubClientFactory _clients;
128+
129+
public ProfileService(GitHubClientFactory clients) => _clients = clients;
130+
131+
public Task<User?> GetUserAsync(string login) =>
132+
_clients
133+
.Create()
134+
.UsingRoute($"/users/{login}")
135+
.WithApiVersion("2022-11-28")
136+
.GetAsync()
137+
.ReadJsonAsync<User>();
138+
}
139+
```
140+
141+
The factory is a singleton and safe to reuse. Each `GitHubClient` it returns is used for one request and discarded. `IHttpClientFactory` keeps the underlying handler pooled, so creating a client per request stays cheap.
142+
143+
### As a Typed Client
144+
145+
You can also register the client directly as a typed client. The container constructs it and injects the configured `HttpClient`.
146+
147+
```csharp
148+
services.AddHttpClient<GitHubClient>(client =>
149+
{
150+
client.BaseAddress = new Uri("https://api.github.com/");
151+
client.DefaultRequestHeaders.UserAgent.ParseAdd("my-app");
152+
});
153+
```
154+
155+
A typed client is registered as transient and constructed from the configured `HttpClient`. You choose the route on it with `UsingRoute`, the same as with the factory. Inject it where you need it.
156+
157+
```csharp
158+
public class StatusService
159+
{
160+
private readonly GitHubClient _client;
161+
162+
public StatusService(GitHubClient client) => _client = client;
163+
164+
public Task<HttpResponseMessage> PingAsync() =>
165+
_client.UsingRoute("/zen").GetAsync();
166+
}
167+
```
168+
169+
:::important Resolve one per request
170+
Because the typed client is still a per-request builder, treat each resolved instance as single use. It is transient, so resolve a fresh one for each request rather than holding it in a longer-lived service and reusing it.
171+
:::
172+
173+
## Choosing Between Them
174+
175+
Use the factory in most cases. It fits the per-request nature of the builder, the long-lived factory produces a fresh client for each request, and the per-request lifetime is obvious at the call site.
176+
177+
Reach for a typed client when you want the least wiring and the consuming service is already scoped to a single request, so a freshly resolved instance is used once and discarded. Outside that, the factory is the safer default.

docs/docs/testing-resources.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 16
2+
sidebar_position: 17
33
title: Testing Resources
44
---
55

0 commit comments

Comments
 (0)