SDK Conventions
This guide covers common patterns and conventions used throughout the Cloudflare.NET SDK.
Pagination
The SDK supports both pagination patterns used by the Cloudflare API. All paginated methods provide two approaches:
- Automatic pagination via
IAsyncEnumerable<T>- handles paging automatically - Manual pagination for fine-grained control over page requests
Page-based Pagination
Page-based pagination uses page and per_page parameters. This pattern is used by:
- DNS Records
- Access Rules (Zone and Account)
- Zone Lockdown Rules
- User-Agent Rules
- Workers KV Namespaces
- D1 Databases
Automatic Pagination
// Iterate over all records automatically
await foreach (var record in cf.Zones.ListAllDnsRecordsAsync(zoneId))
{
Console.WriteLine($"{record.Name}: {record.Content}");
}
// With filters
var filters = new ListDnsRecordsFilters { Type = DnsRecordType.A };
await foreach (var record in cf.Zones.ListAllDnsRecordsAsync(zoneId, filters))
{
// Process A records only
}
Manual Pagination
// Get a specific page
var page = await cf.Zones.ListDnsRecordsAsync(zoneId, new ListDnsRecordsFilters
{
Page = 1,
PerPage = 50
});
// Access pagination info
Console.WriteLine($"Page {page.PageInfo.Page} of {page.PageInfo.TotalPages}");
Console.WriteLine($"Total records: {page.PageInfo.TotalCount}");
// Iterate manually
while (page.PageInfo.Page < page.PageInfo.TotalPages)
{
foreach (var record in page.Items)
{
// Process record
}
// Get next page
page = await cf.Zones.ListDnsRecordsAsync(zoneId, new ListDnsRecordsFilters
{
Page = page.PageInfo.Page + 1,
PerPage = 50
});
}
Cursor-based Pagination
Cursor-based pagination uses an opaque cursor string for continuation. This pattern is used by:
- R2 Buckets
- Rulesets (Zone and Account)
- Workers KV Keys
Automatic Pagination
// Iterate over all buckets automatically
await foreach (var bucket in cf.Accounts.Buckets.ListAllAsync())
{
Console.WriteLine($"Bucket: {bucket.Name}");
}
Manual Pagination
// Get first page
var page = await cf.Accounts.Buckets.ListAsync(new ListR2BucketsFilters
{
PerPage = 50
});
// Process items
foreach (var bucket in page.Items)
{
Console.WriteLine($"Bucket: {bucket.Name}");
}
// Continue if more pages exist
while (!string.IsNullOrEmpty(page.CursorInfo?.Cursor))
{
page = await cf.Accounts.Buckets.ListAsync(new ListR2BucketsFilters
{
PerPage = 50,
Cursor = page.CursorInfo.Cursor
});
foreach (var bucket in page.Items)
{
// Process bucket
}
}
Async Enumerable Pattern
All ListAll*Async methods return IAsyncEnumerable<T>, enabling efficient streaming of results:
// Count records without loading all into memory
var count = await cf.Zones.ListAllDnsRecordsAsync(zoneId).CountAsync();
// Take first N records
var firstTen = await cf.Zones.ListAllDnsRecordsAsync(zoneId).Take(10).ToListAsync();
// Filter with LINQ
var aRecords = await cf.Zones.ListAllDnsRecordsAsync(zoneId)
.Where(r => r.Type == DnsRecordType.A)
.ToListAsync();
Tip
Use System.Linq.Async NuGet package for LINQ extension methods on IAsyncEnumerable<T>.
Cancellation Tokens
All async methods accept an optional CancellationToken:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
var record = await cf.Zones.GetDnsRecordAsync(zoneId, recordId, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation timed out");
}
Filter Objects
List operations typically accept a filter object to narrow results:
// DNS record filters
var dnsFilters = new ListDnsRecordsFilters
{
Type = DnsRecordType.CNAME,
Name = "www.example.com",
Page = 1,
PerPage = 100
};
// Access rule filters
var ruleFilters = new ListAccessRulesFilters
{
Mode = AccessRuleMode.Block,
Page = 1,
PerPage = 50
};
Result Types
Single Results
Methods returning a single item typically return the item directly or null if not found:
// Returns DnsRecord or null
var record = await cf.Zones.FindDnsRecordByNameAsync(zoneId, "www.example.com");
if (record is not null)
{
Console.WriteLine($"Found: {record.Id}");
}
Paginated Results
Paginated methods return wrapper types containing items and pagination info:
- PagePaginatedResult<T> - For page-based pagination
- CursorPaginatedResult<T> - For cursor-based pagination
Error Handling
The SDK throws CloudflareApiException when the API returns success: false:
try
{
await cf.Zones.CreateCnameRecordAsync(zoneId, name, target);
}
catch (CloudflareApiException ex)
{
// API returned an error response
foreach (var error in ex.Errors)
{
Console.WriteLine($"[{error.Code}] {error.Message}");
}
}
catch (HttpRequestException ex)
{
// Network or HTTP-level error
Console.WriteLine($"HTTP Error: {ex.StatusCode} - {ex.Message}");
}
Extensible Enums
The SDK uses extensible enums for values that may be extended by Cloudflare in the future. This pattern provides:
- Strong typing with IntelliSense for known values
- Forward compatibility by accepting unknown string values
- Graceful degradation when new API values are returned
Usage
Extensible enums can be used like regular enums with static properties:
// Use predefined constants with IntelliSense
var location = R2LocationHint.EastNorthAmerica;
var jurisdiction = R2Jurisdiction.EuropeanUnion;
var storageClass = R2StorageClass.InfrequentAccess;
They also support implicit conversion from strings for custom or new values:
// Future-proof: accept values not yet defined in SDK
R2LocationHint futureRegion = "new-region-2025";
R2Jurisdiction customJurisdiction = "custom-jurisdiction";
Comparison
Extensible enums support equality comparison:
if (bucket.Location == R2LocationHint.WestEurope)
{
Console.WriteLine("Bucket is in Western Europe");
}
// Pattern matching works too
if (bucket.StorageClass is { } sc && sc == R2StorageClass.InfrequentAccess)
{
Console.WriteLine("Using infrequent access storage");
}
Available Extensible Enums
R2 Storage
| Type | Purpose | Known Values |
|---|---|---|
R2LocationHint |
Bucket placement hint | wnam, enam, weur, eeur, apac, oc |
R2Jurisdiction |
Data residency | default, eu, fedramp, us |
R2StorageClass |
Storage tier | Standard, InfrequentAccess |
DNS & Zones
| Type | Purpose | Known Values |
|---|---|---|
DnsRecordType |
DNS record type | A, AAAA, CNAME, MX, TXT, NS, SOA, PTR, SRV, HTTPS, SVCB, CAA, DS, DNSKEY, etc. |
ZoneStatus |
Zone activation status | active, pending, initializing, moved, deleted, deactivated |
ZoneSettingId |
Zone setting identifier | ssl, min_tls_version, always_use_https, brotli, http2, http3, development_mode, security_level, etc. |
D1 Databases
| Type | Purpose | Known Values |
|---|---|---|
D1Jurisdiction |
D1 data residency | eu, fedramp |
Security & Firewall
| Type | Purpose | Known Values |
|---|---|---|
AccessRuleMode |
IP access rule action | block, challenge, js_challenge, managed_challenge, whitelist |
AccessRuleTarget |
Access rule target type | ip, ip_range, asn, country |
UaRuleMode |
User-agent rule action | block, challenge, js_challenge, managed_challenge |
LockdownTarget |
Zone lockdown target type | ip, ip_range |
RulesetAction |
WAF ruleset action | block, challenge, js_challenge, managed_challenge, log, skip, execute, rewrite, redirect, route, score, serve_error, compress_response, set_cache_settings, set_config |
ManagedWafOverrideAction |
Managed WAF override | block, challenge, js_challenge, managed_challenge, log, default |
Why Extensible Enums?
Traditional C# enums would fail to deserialize when Cloudflare adds new values:
// Traditional enum - BREAKS if API returns "new-value"
public enum StorageClass { Standard, InfrequentAccess }
// Extensible enum - gracefully handles unknown values
R2StorageClass unknownClass = "new-storage-class-2025"; // Works!
This pattern is commonly used by cloud SDKs (Azure SDK, AWS SDK) to handle API evolution gracefully.
Related
- Getting Started - Quick start guide
- Configuration - SDK configuration options
- API Coverage - Supported API endpoints