Skip to content

Commit c2ea102

Browse files
authored
Merge pull request #35 from Alos-no/develop
Develop
2 parents c2ab275 + b551347 commit c2ea102

17 files changed

+1491
-242
lines changed

docs/articles/configuration.md

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ The resilience pipeline is configured automatically when using `AddCloudflareApi
192192

193193
## Named Clients
194194

195-
For multi-account scenarios, use named clients with separate configurations:
195+
For multi-account scenarios where configurations are known at startup, use named clients:
196196

197197
```csharp
198198
// appsettings.json
@@ -212,6 +212,78 @@ builder.Services.AddCloudflareApiClient("production", builder.Configuration);
212212
builder.Services.AddCloudflareApiClient("staging", builder.Configuration);
213213
```
214214

215+
## Dynamic Clients
216+
217+
For scenarios where client configurations are not known at startup (e.g., desktop applications where users add accounts at runtime), use dynamic client creation:
218+
219+
```csharp
220+
public class AccountManager(ICloudflareApiClientFactory factory)
221+
{
222+
public async Task<AccountInfo> ValidateAndGetAccountInfoAsync(string apiToken, string accountId)
223+
{
224+
var options = new CloudflareApiOptions
225+
{
226+
ApiToken = apiToken,
227+
AccountId = accountId,
228+
DefaultTimeout = TimeSpan.FromSeconds(15),
229+
RateLimiting = new RateLimitingOptions
230+
{
231+
IsEnabled = true,
232+
PermitLimit = 5, // Conservative for user-provided credentials
233+
MaxRetries = 2
234+
}
235+
};
236+
237+
// Create and use a dynamic client
238+
using var client = factory.CreateClient(options);
239+
240+
var user = await client.User.GetUserAsync();
241+
return new AccountInfo(user.Email, accountId);
242+
}
243+
}
244+
```
245+
246+
### Dynamic Client Characteristics
247+
248+
| Aspect | Named Clients | Dynamic Clients |
249+
|--------|---------------|-----------------|
250+
| Configuration | At startup via DI | At runtime via `CloudflareApiOptions` |
251+
| Lifecycle | Managed by DI container | Must be disposed by caller |
252+
| HttpClient | Shared via `IHttpClientFactory` | Owned by the client instance |
253+
| Resilience Pipeline | Shared per named client | Isolated per instance |
254+
| Use Case | Known accounts, server apps | User-provided accounts, desktop apps |
255+
256+
### Disposal Requirements
257+
258+
Dynamic clients implement `IDisposable` and **must** be disposed when no longer needed:
259+
260+
```csharp
261+
// Option 1: using statement (recommended)
262+
using var client = factory.CreateClient(options);
263+
await client.Zones.ListZonesAsync();
264+
// Client automatically disposed at end of scope
265+
266+
// Option 2: using declaration in async methods
267+
using var client = factory.CreateClient(options);
268+
var zones = await client.Zones.ListZonesAsync();
269+
// ... more operations
270+
// Client disposed when method returns
271+
272+
// Option 3: Manual disposal (for longer-lived clients)
273+
var client = factory.CreateClient(options);
274+
try
275+
{
276+
// Use client for multiple operations...
277+
}
278+
finally
279+
{
280+
client.Dispose();
281+
}
282+
```
283+
284+
> [!WARNING]
285+
> Failing to dispose dynamic clients will leak `HttpClient` instances and their underlying socket connections. Always use a `using` statement or explicitly call `Dispose()`.
286+
215287
## Environment Variables
216288

217289
Configuration can also be provided via environment variables using the `__` (double underscore) convention:

docs/articles/getting-started.md

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,40 @@ Install-Package Cloudflare.NET.Analytics
4141

4242
## Configuration
4343

44+
The SDK supports two configuration approaches:
45+
46+
1. **Dependency Injection** (recommended for most applications) - Register clients at startup with `AddCloudflareApiClient()`
47+
2. **Dynamic Creation** - Create clients at runtime when configurations aren't known at startup (see [Dynamic Clients](#option-2-dynamic-clients-runtime-creation))
48+
49+
### Dependency Injection
50+
51+
Register the clients in your `Program.cs`:
52+
53+
```csharp
54+
var builder = WebApplication.CreateBuilder(args);
55+
56+
// Register from IConfiguration (binds to "Cloudflare" and "R2" sections)
57+
builder.Services.AddCloudflareApiClient(builder.Configuration);
58+
builder.Services.AddCloudflareR2Client(builder.Configuration);
59+
builder.Services.AddCloudflareAnalytics();
60+
61+
var app = builder.Build();
62+
```
63+
64+
Alternatively, configure options programmatically:
65+
66+
```csharp
67+
builder.Services.AddCloudflareApiClient(options =>
68+
{
69+
options.ApiToken = "your-api-token";
70+
options.AccountId = "your-account-id";
71+
options.DefaultTimeout = TimeSpan.FromSeconds(30);
72+
options.RateLimiting.IsEnabled = true;
73+
options.RateLimiting.EnableProactiveThrottling = true; // Delay requests when quota is low
74+
options.RateLimiting.QuotaLowThreshold = 0.1; // Throttle at 10% remaining
75+
});
76+
```
77+
4478
### appsettings.json
4579

4680
Configure your Cloudflare credentials in `appsettings.json`:
@@ -73,35 +107,6 @@ Configure your Cloudflare credentials in `appsettings.json`:
73107
> [!NOTE]
74108
> Never commit API tokens or secrets to source control. Use [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) for development and environment variables or a managed Key Vault for production.
75109
76-
### Dependency Injection
77-
78-
Register the clients in your `Program.cs`:
79-
80-
```csharp
81-
var builder = WebApplication.CreateBuilder(args);
82-
83-
// Register from IConfiguration (binds to "Cloudflare" and "R2" sections)
84-
builder.Services.AddCloudflareApiClient(builder.Configuration);
85-
builder.Services.AddCloudflareR2Client(builder.Configuration);
86-
builder.Services.AddCloudflareAnalytics();
87-
88-
var app = builder.Build();
89-
```
90-
91-
Alternatively, configure options programmatically:
92-
93-
```csharp
94-
builder.Services.AddCloudflareApiClient(options =>
95-
{
96-
options.ApiToken = "your-api-token";
97-
options.AccountId = "your-account-id";
98-
options.DefaultTimeout = TimeSpan.FromSeconds(30);
99-
options.RateLimiting.IsEnabled = true;
100-
options.RateLimiting.EnableProactiveThrottling = true; // Delay requests when quota is low
101-
options.RateLimiting.QuotaLowThreshold = 0.1; // Throttle at 10% remaining
102-
});
103-
```
104-
105110
## Basic Usage
106111

107112
Inject <xref:Cloudflare.NET.ICloudflareApiClient> into your services:
@@ -131,7 +136,11 @@ public class DnsService(ICloudflareApiClient cf)
131136

132137
## Multi-Account Support
133138

134-
For applications managing multiple Cloudflare accounts, use named clients:
139+
For applications managing multiple Cloudflare accounts, you have two options:
140+
141+
### Option 1: Named Clients (Pre-Registered)
142+
143+
Use named clients when account configurations are known at startup:
135144

136145
```csharp
137146
// Register named clients
@@ -148,7 +157,7 @@ builder.Services.AddCloudflareApiClient("staging", options =>
148157
});
149158
```
150159

151-
### Using the Factory
160+
#### Using the Factory
152161

153162
```csharp
154163
public class MultiAccountService(ICloudflareApiClientFactory apiFactory)
@@ -161,7 +170,7 @@ public class MultiAccountService(ICloudflareApiClientFactory apiFactory)
161170
}
162171
```
163172

164-
### Using Keyed Services (.NET 8+)
173+
#### Using Keyed Services (.NET 8+)
165174

166175
```csharp
167176
public class MyService(
@@ -172,6 +181,42 @@ public class MyService(
172181
}
173182
```
174183

184+
### Option 2: Dynamic Clients (Runtime Creation)
185+
186+
Use dynamic clients when account configurations are not known at startup, such as when users can add Cloudflare accounts through a UI:
187+
188+
```csharp
189+
public class UserAccountService(ICloudflareApiClientFactory factory)
190+
{
191+
public async Task<IReadOnlyList<Zone>> GetUserZonesAsync(UserCloudflareCredentials credentials)
192+
{
193+
var options = new CloudflareApiOptions
194+
{
195+
ApiToken = credentials.ApiToken,
196+
AccountId = credentials.AccountId,
197+
RateLimiting = new RateLimitingOptions
198+
{
199+
IsEnabled = true,
200+
PermitLimit = 10 // Conservative limit for user accounts
201+
}
202+
};
203+
204+
// Dynamic clients must be disposed when done
205+
using var client = factory.CreateClient(options);
206+
207+
var zones = new List<Zone>();
208+
await foreach (var zone in client.Zones.ListAllZonesAsync())
209+
{
210+
zones.Add(zone);
211+
}
212+
return zones;
213+
}
214+
}
215+
```
216+
217+
> [!IMPORTANT]
218+
> Dynamic clients manage their own `HttpClient` and resilience pipeline. Always dispose them when finished using a `using` statement or by calling `Dispose()`. Each dynamic client has isolated state (rate limiter, circuit breaker) and does not share resources with other clients.
219+
175220
## Error Handling
176221

177222
The SDK throws <xref:Cloudflare.NET.Core.Exceptions.CloudflareApiException> when the API returns an error:

src/Cloudflare.NET.R2/Cloudflare.NET.R2.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<ItemGroup>
1919
<!-- This package will use the official AWS SDK for S3 operations. -->
2020
<PackageReference Include="AWSSDK.S3" Version="4.0.7.1" />
21+
<!-- Explicit Core version to fix vulnerability GHSA-9cvc-h2w8-phrp (fixed in 4.0.3.3) -->
22+
<PackageReference Include="AWSSDK.Core" Version="4.0.3.3" />
2123
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
2224
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
2325
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />

src/Cloudflare.NET/Cloudflare.NET.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<!-- NuGet Package Properties (common properties inherited from Directory.Build.props) -->
77
<PackageId>Cloudflare.NET.Api</PackageId>
8-
<Version>3.3.0</Version>
8+
<Version>3.4.0</Version>
99
<Description>Cloudflare.NET - A comprehensive C# client library for the Cloudflare REST API. Manage DNS records, Zones, R2 buckets, Workers, WAF rules, Turnstile, and security features with strongly-typed .NET code.</Description>
1010
<PackageTags>cloudflare;cloudflare-api;cloudflare-sdk;cloudflare-client;dotnet;csharp;dns;r2;waf;firewall;zone;workers;turnstile;api-client;rest-client</PackageTags>
1111
</PropertyGroup>

src/Cloudflare.NET/CloudflareApiClient.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,31 @@ public CloudflareApiClient(HttpClient httpClient, IOptions<CloudflareApiOptions>
115115
public ITurnstileApi Turnstile => _turnstile.Value;
116116

117117
#endregion
118+
119+
120+
#region Methods Impl - IDisposable
121+
122+
/// <summary>
123+
/// Disposes this client instance.
124+
/// </summary>
125+
/// <remarks>
126+
/// <para>
127+
/// For DI-managed clients, this method does nothing because the <see cref="HttpClient" />
128+
/// lifetime is managed by <see cref="IHttpClientFactory" />. The factory handles pooling
129+
/// and disposal of the underlying handlers automatically.
130+
/// </para>
131+
/// <para>
132+
/// For dynamic clients created via
133+
/// <see cref="ICloudflareApiClientFactory.CreateClient(CloudflareApiOptions)" />,
134+
/// the <see cref="DynamicCloudflareApiClient" /> wrapper handles actual disposal of resources.
135+
/// </para>
136+
/// </remarks>
137+
public void Dispose()
138+
{
139+
// No-op for DI-managed clients. The HttpClient is managed by IHttpClientFactory
140+
// and should not be disposed by the client. For dynamic clients, the
141+
// DynamicCloudflareApiClient wrapper handles disposal.
142+
}
143+
144+
#endregion
118145
}

src/Cloudflare.NET/Core/CloudflareApiClientFactory.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace Cloudflare.NET.Core;
33
using Internal;
44
using Microsoft.Extensions.Logging;
55
using Microsoft.Extensions.Options;
6+
using Resilience;
67
using Validation;
78

89
/// <summary>
@@ -79,6 +80,47 @@ public ICloudflareApiClient CreateClient(string name)
7980
return new CloudflareApiClient(httpClient, optionsWrapper, _loggerFactory);
8081
}
8182

83+
84+
/// <inheritdoc />
85+
public ICloudflareApiClient CreateClient(CloudflareApiOptions options)
86+
{
87+
ArgumentNullException.ThrowIfNull(options);
88+
89+
// Validate the options using the shared validator for consistent, clear error messages.
90+
ValidateNamedClientConfiguration("dynamic", options);
91+
92+
// Build the resilience pipeline using the shared builder.
93+
// This ensures the same resilience behavior as DI-registered clients.
94+
var logger = _loggerFactory.CreateLogger(LoggingConstants.Categories.HttpResilience);
95+
var pipeline = CloudflareResiliencePipelineBuilder.Build(options, logger, clientName: "dynamic");
96+
97+
// Create the handler chain: ResilienceHandler → SocketsHttpHandler.
98+
// SocketsHttpHandler is used for proper connection pooling and lifetime management.
99+
var socketHandler = new SocketsHttpHandler
100+
{
101+
// PooledConnectionLifetime ensures connections are recycled periodically,
102+
// which helps with DNS changes and prevents stale connections.
103+
// This mirrors the behavior of IHttpClientFactory-managed handlers.
104+
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
105+
};
106+
107+
var resilienceHandler = new ResilienceDelegatingHandler(pipeline, socketHandler);
108+
109+
// Create and configure the HttpClient using the shared configurator.
110+
// This ensures the same configuration as DI-registered clients.
111+
var httpClient = new HttpClient(resilienceHandler);
112+
CloudflareHttpClientConfigurator.Configure(httpClient, options, setAuthorizationHeader: true);
113+
114+
// Wrap the options in IOptions<T> for the CloudflareApiClient constructor.
115+
var optionsWrapper = new NamedOptionsWrapper<CloudflareApiOptions>(options);
116+
117+
// Create the inner client that handles all API operations.
118+
var innerClient = new CloudflareApiClient(httpClient, optionsWrapper, _loggerFactory);
119+
120+
// Wrap in DynamicCloudflareApiClient which handles disposal of the owned HttpClient.
121+
return new DynamicCloudflareApiClient(innerClient, httpClient);
122+
}
123+
82124
#endregion
83125

84126
#region Methods

0 commit comments

Comments
 (0)