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
74 changes: 73 additions & 1 deletion docs/articles/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ The resilience pipeline is configured automatically when using `AddCloudflareApi

## Named Clients

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

```csharp
// appsettings.json
Expand All @@ -212,6 +212,78 @@ builder.Services.AddCloudflareApiClient("production", builder.Configuration);
builder.Services.AddCloudflareApiClient("staging", builder.Configuration);
```

## Dynamic Clients

For scenarios where client configurations are not known at startup (e.g., desktop applications where users add accounts at runtime), use dynamic client creation:

```csharp
public class AccountManager(ICloudflareApiClientFactory factory)
{
public async Task<AccountInfo> ValidateAndGetAccountInfoAsync(string apiToken, string accountId)
{
var options = new CloudflareApiOptions
{
ApiToken = apiToken,
AccountId = accountId,
DefaultTimeout = TimeSpan.FromSeconds(15),
RateLimiting = new RateLimitingOptions
{
IsEnabled = true,
PermitLimit = 5, // Conservative for user-provided credentials
MaxRetries = 2
}
};

// Create and use a dynamic client
using var client = factory.CreateClient(options);

var user = await client.User.GetUserAsync();
return new AccountInfo(user.Email, accountId);
}
}
```

### Dynamic Client Characteristics

| Aspect | Named Clients | Dynamic Clients |
|--------|---------------|-----------------|
| Configuration | At startup via DI | At runtime via `CloudflareApiOptions` |
| Lifecycle | Managed by DI container | Must be disposed by caller |
| HttpClient | Shared via `IHttpClientFactory` | Owned by the client instance |
| Resilience Pipeline | Shared per named client | Isolated per instance |
| Use Case | Known accounts, server apps | User-provided accounts, desktop apps |

### Disposal Requirements

Dynamic clients implement `IDisposable` and **must** be disposed when no longer needed:

```csharp
// Option 1: using statement (recommended)
using var client = factory.CreateClient(options);
await client.Zones.ListZonesAsync();
// Client automatically disposed at end of scope

// Option 2: using declaration in async methods
using var client = factory.CreateClient(options);
var zones = await client.Zones.ListZonesAsync();
// ... more operations
// Client disposed when method returns

// Option 3: Manual disposal (for longer-lived clients)
var client = factory.CreateClient(options);
try
{
// Use client for multiple operations...
}
finally
{
client.Dispose();
}
```

> [!WARNING]
> Failing to dispose dynamic clients will leak `HttpClient` instances and their underlying socket connections. Always use a `using` statement or explicitly call `Dispose()`.

## Environment Variables

Configuration can also be provided via environment variables using the `__` (double underscore) convention:
Expand Down
109 changes: 77 additions & 32 deletions docs/articles/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,40 @@ Install-Package Cloudflare.NET.Analytics

## Configuration

The SDK supports two configuration approaches:

1. **Dependency Injection** (recommended for most applications) - Register clients at startup with `AddCloudflareApiClient()`
2. **Dynamic Creation** - Create clients at runtime when configurations aren't known at startup (see [Dynamic Clients](#option-2-dynamic-clients-runtime-creation))

### Dependency Injection

Register the clients in your `Program.cs`:

```csharp
var builder = WebApplication.CreateBuilder(args);

// Register from IConfiguration (binds to "Cloudflare" and "R2" sections)
builder.Services.AddCloudflareApiClient(builder.Configuration);
builder.Services.AddCloudflareR2Client(builder.Configuration);
builder.Services.AddCloudflareAnalytics();

var app = builder.Build();
```

Alternatively, configure options programmatically:

```csharp
builder.Services.AddCloudflareApiClient(options =>
{
options.ApiToken = "your-api-token";
options.AccountId = "your-account-id";
options.DefaultTimeout = TimeSpan.FromSeconds(30);
options.RateLimiting.IsEnabled = true;
options.RateLimiting.EnableProactiveThrottling = true; // Delay requests when quota is low
options.RateLimiting.QuotaLowThreshold = 0.1; // Throttle at 10% remaining
});
```

### appsettings.json

Configure your Cloudflare credentials in `appsettings.json`:
Expand Down Expand Up @@ -73,35 +107,6 @@ Configure your Cloudflare credentials in `appsettings.json`:
> [!NOTE]
> 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.

### Dependency Injection

Register the clients in your `Program.cs`:

```csharp
var builder = WebApplication.CreateBuilder(args);

// Register from IConfiguration (binds to "Cloudflare" and "R2" sections)
builder.Services.AddCloudflareApiClient(builder.Configuration);
builder.Services.AddCloudflareR2Client(builder.Configuration);
builder.Services.AddCloudflareAnalytics();

var app = builder.Build();
```

Alternatively, configure options programmatically:

```csharp
builder.Services.AddCloudflareApiClient(options =>
{
options.ApiToken = "your-api-token";
options.AccountId = "your-account-id";
options.DefaultTimeout = TimeSpan.FromSeconds(30);
options.RateLimiting.IsEnabled = true;
options.RateLimiting.EnableProactiveThrottling = true; // Delay requests when quota is low
options.RateLimiting.QuotaLowThreshold = 0.1; // Throttle at 10% remaining
});
```

## Basic Usage

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

## Multi-Account Support

For applications managing multiple Cloudflare accounts, use named clients:
For applications managing multiple Cloudflare accounts, you have two options:

### Option 1: Named Clients (Pre-Registered)

Use named clients when account configurations are known at startup:

```csharp
// Register named clients
Expand All @@ -148,7 +157,7 @@ builder.Services.AddCloudflareApiClient("staging", options =>
});
```

### Using the Factory
#### Using the Factory

```csharp
public class MultiAccountService(ICloudflareApiClientFactory apiFactory)
Expand All @@ -161,7 +170,7 @@ public class MultiAccountService(ICloudflareApiClientFactory apiFactory)
}
```

### Using Keyed Services (.NET 8+)
#### Using Keyed Services (.NET 8+)

```csharp
public class MyService(
Expand All @@ -172,6 +181,42 @@ public class MyService(
}
```

### Option 2: Dynamic Clients (Runtime Creation)

Use dynamic clients when account configurations are not known at startup, such as when users can add Cloudflare accounts through a UI:

```csharp
public class UserAccountService(ICloudflareApiClientFactory factory)
{
public async Task<IReadOnlyList<Zone>> GetUserZonesAsync(UserCloudflareCredentials credentials)
{
var options = new CloudflareApiOptions
{
ApiToken = credentials.ApiToken,
AccountId = credentials.AccountId,
RateLimiting = new RateLimitingOptions
{
IsEnabled = true,
PermitLimit = 10 // Conservative limit for user accounts
}
};

// Dynamic clients must be disposed when done
using var client = factory.CreateClient(options);

var zones = new List<Zone>();
await foreach (var zone in client.Zones.ListAllZonesAsync())
{
zones.Add(zone);
}
return zones;
}
}
```

> [!IMPORTANT]
> 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.

## Error Handling

The SDK throws <xref:Cloudflare.NET.Core.Exceptions.CloudflareApiException> when the API returns an error:
Expand Down
2 changes: 2 additions & 0 deletions src/Cloudflare.NET.R2/Cloudflare.NET.R2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<ItemGroup>
<!-- This package will use the official AWS SDK for S3 operations. -->
<PackageReference Include="AWSSDK.S3" Version="4.0.7.1" />
<!-- Explicit Core version to fix vulnerability GHSA-9cvc-h2w8-phrp (fixed in 4.0.3.3) -->
<PackageReference Include="AWSSDK.Core" Version="4.0.3.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
Expand Down
2 changes: 1 addition & 1 deletion src/Cloudflare.NET/Cloudflare.NET.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<!-- NuGet Package Properties (common properties inherited from Directory.Build.props) -->
<PackageId>Cloudflare.NET.Api</PackageId>
<Version>3.3.0</Version>
<Version>3.4.0</Version>
<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>
<PackageTags>cloudflare;cloudflare-api;cloudflare-sdk;cloudflare-client;dotnet;csharp;dns;r2;waf;firewall;zone;workers;turnstile;api-client;rest-client</PackageTags>
</PropertyGroup>
Expand Down
27 changes: 27 additions & 0 deletions src/Cloudflare.NET/CloudflareApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,31 @@ public CloudflareApiClient(HttpClient httpClient, IOptions<CloudflareApiOptions>
public ITurnstileApi Turnstile => _turnstile.Value;

#endregion


#region Methods Impl - IDisposable

/// <summary>
/// Disposes this client instance.
/// </summary>
/// <remarks>
/// <para>
/// For DI-managed clients, this method does nothing because the <see cref="HttpClient" />
/// lifetime is managed by <see cref="IHttpClientFactory" />. The factory handles pooling
/// and disposal of the underlying handlers automatically.
/// </para>
/// <para>
/// For dynamic clients created via
/// <see cref="ICloudflareApiClientFactory.CreateClient(CloudflareApiOptions)" />,
/// the <see cref="DynamicCloudflareApiClient" /> wrapper handles actual disposal of resources.
/// </para>
/// </remarks>
public void Dispose()
{
// No-op for DI-managed clients. The HttpClient is managed by IHttpClientFactory
// and should not be disposed by the client. For dynamic clients, the
// DynamicCloudflareApiClient wrapper handles disposal.
}

#endregion
}
42 changes: 42 additions & 0 deletions src/Cloudflare.NET/Core/CloudflareApiClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Cloudflare.NET.Core;
using Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Resilience;
using Validation;

/// <summary>
Expand Down Expand Up @@ -79,6 +80,47 @@ public ICloudflareApiClient CreateClient(string name)
return new CloudflareApiClient(httpClient, optionsWrapper, _loggerFactory);
}


/// <inheritdoc />
public ICloudflareApiClient CreateClient(CloudflareApiOptions options)
{
ArgumentNullException.ThrowIfNull(options);

// Validate the options using the shared validator for consistent, clear error messages.
ValidateNamedClientConfiguration("dynamic", options);

// Build the resilience pipeline using the shared builder.
// This ensures the same resilience behavior as DI-registered clients.
var logger = _loggerFactory.CreateLogger(LoggingConstants.Categories.HttpResilience);
var pipeline = CloudflareResiliencePipelineBuilder.Build(options, logger, clientName: "dynamic");

// Create the handler chain: ResilienceHandler → SocketsHttpHandler.
// SocketsHttpHandler is used for proper connection pooling and lifetime management.
var socketHandler = new SocketsHttpHandler
{
// PooledConnectionLifetime ensures connections are recycled periodically,
// which helps with DNS changes and prevents stale connections.
// This mirrors the behavior of IHttpClientFactory-managed handlers.
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
};

var resilienceHandler = new ResilienceDelegatingHandler(pipeline, socketHandler);

// Create and configure the HttpClient using the shared configurator.
// This ensures the same configuration as DI-registered clients.
var httpClient = new HttpClient(resilienceHandler);
CloudflareHttpClientConfigurator.Configure(httpClient, options, setAuthorizationHeader: true);

// Wrap the options in IOptions<T> for the CloudflareApiClient constructor.
var optionsWrapper = new NamedOptionsWrapper<CloudflareApiOptions>(options);

// Create the inner client that handles all API operations.
var innerClient = new CloudflareApiClient(httpClient, optionsWrapper, _loggerFactory);

// Wrap in DynamicCloudflareApiClient which handles disposal of the owned HttpClient.
return new DynamicCloudflareApiClient(innerClient, httpClient);
}

#endregion

#region Methods
Expand Down
Loading