TL;DR: The Cloudflare.NET SDK lets you automate DNS for multi-tenant apps: provision tenant subdomains, onboard customer vanity domains with automatic SSL, and migrate zones via BIND import/export, all from C# with full IntelliSense.
If you're building a multi-tenant SaaS application, you've probably hit this wall: DNS changes don't scale with manual dashboard clicks.
Every new tenant needs a subdomain. Every enterprise customer wants their vanity domain. Every migration involves exporting zone files and hoping nothing breaks. And somewhere along the way, DNS became the one piece of infrastructure that isn't automated.
The Cloudflare.NET SDK brings DNS management into your .NET codebase. This article covers three common scenarios.
Why a Typed SDK?
A typed SDK gives you:
- IntelliSense everywhere - Method signatures, parameter types, and return types are discoverable in your IDE
- Compile-time safety - Catch typos and type mismatches before runtime
- Comprehensive XML documentation - Every method, parameter, and model is documented inline
- Online API reference - Searchable documentation with examples
Compare this to shelling out to CLI tools or writing raw HTTP requests. When you're managing hundreds of DNS records, the compiler catching a typo beats debugging a 400 response at 2am.
Scenario 1: Multi-Tenant Subdomain Provisioning
The Problem
You're building a SaaS platform where each tenant gets a subdomain:
acme.yourapp.comglobex.yourapp.cominitech.yourapp.com
When a customer signs up, you need to create their DNS record automatically. When they cancel, you need to clean it up. Doing this manually doesn't scale past a dozen tenants.
The Solution
Automate DNS provisioning as part of your tenant onboarding flow:
public class TenantProvisioningService(
ICloudflareApiClient cloudflare,
ILogger<TenantProvisioningService> logger)
{
private readonly string _zoneId = "your-zone-id";
private readonly string _baseDomain = "yourapp.com";
private readonly string _appServerIp = "203.0.113.50";
public async Task<TenantProvisioningResult> ProvisionTenantAsync(string tenantSlug)
{
var subdomain = $"{tenantSlug}.{_baseDomain}";
logger.LogInformation("Provisioning DNS for tenant: {Subdomain}", subdomain);
// Create the A record pointing to your application server
var record = await cloudflare.Dns.CreateDnsRecordAsync(_zoneId,
new CreateDnsRecordRequest(
Type: DnsRecordType.A,
Name: subdomain,
Content: _appServerIp,
Proxied: true, // Enable Cloudflare's CDN and security
Ttl: 1 // Automatic TTL when proxied
));
logger.LogInformation("Created DNS record {Id} for {Subdomain}", record.Id, subdomain);
return new TenantProvisioningResult(
TenantSlug: tenantSlug,
Subdomain: subdomain,
DnsRecordId: record.Id,
IsProxied: record.Proxied
);
}
public async Task DeprovisionTenantAsync(string tenantSlug, string dnsRecordId)
{
logger.LogInformation("Removing DNS for tenant: {TenantSlug}", tenantSlug);
await cloudflare.Dns.DeleteDnsRecordAsync(_zoneId, dnsRecordId);
logger.LogInformation("Deleted DNS record {Id}", dnsRecordId);
}
}
public record TenantProvisioningResult(
string TenantSlug,
string Subdomain,
string DnsRecordId,
bool IsProxied
);
Batch Provisioning
Onboarding a batch of tenants? Use the batch API for atomic operations:
public async Task ProvisionTenantsAsync(IEnumerable<string> tenantSlugs)
{
var createRequests = tenantSlugs
.Select(slug => new CreateDnsRecordRequest(
Type: DnsRecordType.A,
Name: $"{slug}.{_baseDomain}",
Content: _appServerIp,
Proxied: true))
.ToList();
// All records created atomically - if one fails, none are created
var result = await cloudflare.Dns.BatchDnsRecordsAsync(_zoneId,
new BatchDnsRecordsRequest(Posts: createRequests));
logger.LogInformation("Batch created {Count} tenant DNS records", result.Posts?.Count ?? 0);
}
The batch API executes operations in a guaranteed order (Deletes → Patches → Puts → Posts) within a single database transaction. If any operation fails, the entire batch rolls back.
Find and Update Existing Records
Need to update a tenant's DNS (e.g., migrating to a new server)?
public async Task UpdateTenantIpAsync(string tenantSlug, string newIpAddress)
{
var subdomain = $"{tenantSlug}.{_baseDomain}";
// Find the existing record by hostname
var existing = await cloudflare.Dns.FindDnsRecordByNameAsync(
_zoneId,
subdomain,
type: DnsRecordType.A);
if (existing is null)
{
throw new InvalidOperationException($"No DNS record found for {subdomain}");
}
// Patch only the content field
await cloudflare.Dns.PatchDnsRecordAsync(_zoneId, existing.Id,
new PatchDnsRecordRequest(Content: newIpAddress));
logger.LogInformation("Updated {Subdomain} to point to {Ip}", subdomain, newIpAddress);
}
Scenario 2: White-Label Custom Domains (Cloudflare for SaaS)
The Problem
Your enterprise customers want their own vanity domains:
- Instead of
acme.yourapp.com, Acme Corp wantsapp.acme.com - This requires SSL certificates for domains you don't control
- You need to validate domain ownership before issuing certificates
- The customer needs clear instructions for their DNS changes
Cloudflare for SaaS handles this. The SDK provides full access to the Custom Hostnames API.
The Solution
Implement a custom domain onboarding workflow:
public class CustomDomainService(
ICloudflareApiClient cloudflare,
ILogger<CustomDomainService> logger)
{
private readonly string _zoneId = "your-zone-id";
private readonly string _fallbackOrigin = "origin.yourapp.com";
public async Task<CustomDomainOnboardingResult> StartOnboardingAsync(
string customerId,
string customerDomain)
{
logger.LogInformation(
"Starting custom domain onboarding for {Customer}: {Domain}",
customerId, customerDomain);
// Configure SSL with TXT-based domain control validation
var sslConfig = new SslConfiguration(
Method: DcvMethod.Txt,
Type: CertificateType.Dv,
Settings: new SslSettings(
MinTlsVersion: MinTlsVersion.Tls12,
Http2: SslToggle.On
)
);
// Create the custom hostname
var hostname = await cloudflare.Zones.CustomHostnames.CreateAsync(_zoneId,
new CreateCustomHostnameRequest(
Hostname: customerDomain,
Ssl: sslConfig
));
logger.LogInformation(
"Created custom hostname {Id} with status {Status}",
hostname.Id, hostname.Status);
// Build customer instructions
var instructions = BuildCustomerInstructions(hostname);
return new CustomDomainOnboardingResult(
CustomHostnameId: hostname.Id,
Status: hostname.Status.ToString(),
SslStatus: hostname.Ssl.Status.ToString(),
CustomerInstructions: instructions
);
}
private CustomerDnsInstructions BuildCustomerInstructions(CustomHostname hostname)
{
var instructions = new CustomerDnsInstructions();
// TXT record for domain ownership verification
if (hostname.OwnershipVerification is not null)
{
instructions.OwnershipTxtRecord = new DnsInstruction(
Type: "TXT",
Name: hostname.OwnershipVerification.Name,
Value: hostname.OwnershipVerification.Value,
Purpose: "Proves you own this domain"
);
}
// TXT record for SSL certificate validation
if (hostname.Ssl.ValidationRecords is { Count: > 0 })
{
var sslValidation = hostname.Ssl.ValidationRecords[0];
if (sslValidation.TxtName is not null)
{
instructions.SslTxtRecord = new DnsInstruction(
Type: "TXT",
Name: sslValidation.TxtName,
Value: sslValidation.TxtValue!,
Purpose: "Validates SSL certificate issuance"
);
}
}
// CNAME to route traffic (after validation completes)
instructions.CnameRecord = new DnsInstruction(
Type: "CNAME",
Name: hostname.Hostname,
Value: _fallbackOrigin,
Purpose: "Routes traffic through Cloudflare to your app"
);
return instructions;
}
}
public record CustomDomainOnboardingResult(
string CustomHostnameId,
string Status,
string SslStatus,
CustomerDnsInstructions CustomerInstructions
);
public record CustomerDnsInstructions
{
public DnsInstruction? OwnershipTxtRecord { get; set; }
public DnsInstruction? SslTxtRecord { get; set; }
public DnsInstruction? CnameRecord { get; set; }
}
public record DnsInstruction(string Type, string Name, string Value, string Purpose);
Poll for Validation Status
After the customer adds their DNS records, poll for validation (add to CustomDomainService):
public async Task<CustomDomainStatus> CheckValidationStatusAsync(string customHostnameId)
{
var hostname = await cloudflare.Zones.CustomHostnames.GetAsync(_zoneId, customHostnameId);
var result = new CustomDomainStatus(
Hostname: hostname.Hostname,
OverallStatus: hostname.Status,
SslStatus: hostname.Ssl.Status,
IsFullyActive: hostname.Status == CustomHostnameStatus.Active
&& hostname.Ssl.Status == SslStatus.Active,
VerificationErrors: hostname.VerificationErrors
);
if (result.IsFullyActive)
{
logger.LogInformation("Custom domain {Hostname} is fully active!", hostname.Hostname);
}
else if (hostname.VerificationErrors is { Count: > 0 })
{
logger.LogWarning(
"Validation errors for {Hostname}: {Errors}",
hostname.Hostname,
string.Join(", ", hostname.VerificationErrors));
}
return result;
}
public record CustomDomainStatus(
string Hostname,
CustomHostnameStatus OverallStatus,
SslStatus SslStatus,
bool IsFullyActive,
IReadOnlyList<string>? VerificationErrors
);
List All Custom Domains
Enumerate all custom hostnames with automatic pagination (add to CustomDomainService):
public async Task<IReadOnlyList<CustomDomainSummary>> ListAllCustomDomainsAsync()
{
var domains = new List<CustomDomainSummary>();
await foreach (var hostname in cloudflare.Zones.CustomHostnames.ListAllAsync(_zoneId))
{
domains.Add(new CustomDomainSummary(
Id: hostname.Id,
Hostname: hostname.Hostname,
Status: hostname.Status,
SslStatus: hostname.Ssl.Status,
CreatedAt: hostname.CreatedAt
));
}
return domains;
}
public record CustomDomainSummary(
string Id,
string Hostname,
CustomHostnameStatus Status,
SslStatus SslStatus,
DateTimeOffset? CreatedAt
);
Scenario 3: DNS Zone Migration via BIND Import/Export
The Problem
You're migrating a domain to Cloudflare from another DNS provider. You have hundreds of DNS records. Manual recreation is error-prone and tedious.
Or you're moving away from Cloudflare and need to export your zone file.
The Solution
The SDK supports BIND zone file import/export - the industry standard for DNS record interchange.
Export from Cloudflare
// Standalone helper or add to a service class
public async Task ExportZoneToFileAsync(string zoneId, string outputPath)
{
logger.LogInformation("Exporting DNS zone {ZoneId} to BIND format", zoneId);
var bindContent = await cloudflare.Dns.ExportDnsRecordsAsync(zoneId);
await File.WriteAllTextAsync(outputPath, bindContent);
logger.LogInformation("Exported zone to {Path}", outputPath);
}
The exported file looks like standard BIND format:
;;
;; Domain: example.com.
;; Exported: 2025-01-15 10:30:00
;;
$ORIGIN example.com.
$TTL 300
@ IN A 203.0.113.50
www IN CNAME example.com.
api IN A 203.0.113.51
mail IN MX 10 mail.example.com.
Import to Cloudflare
// Standalone helper or add to a service class
public async Task ImportZoneFromFileAsync(string zoneId, string inputPath, bool proxied = false)
{
logger.LogInformation("Importing DNS zone from {Path}", inputPath);
var bindContent = await File.ReadAllTextAsync(inputPath);
var result = await cloudflare.Dns.ImportDnsRecordsAsync(zoneId, bindContent, proxied);
logger.LogInformation(
"Import complete: {Added} records added, {Parsed} total parsed",
result.RecordsAdded,
result.TotalRecordsParsed);
}
Migration Workflow
Combine export and import for a complete migration:
public class DnsMigrationService(
ICloudflareApiClient cloudflare,
ILogger<DnsMigrationService> logger)
{
public async Task MigrateZoneAsync(
string sourceZoneId,
string targetZoneId,
bool enableProxy = false)
{
logger.LogInformation(
"Migrating DNS from zone {Source} to {Target}",
sourceZoneId, targetZoneId);
// Step 1: Export source zone
var bindContent = await cloudflare.Dns.ExportDnsRecordsAsync(sourceZoneId);
logger.LogInformation("Exported {Length} bytes of BIND data", bindContent.Length);
// Step 2: Import to target zone
var result = await cloudflare.Dns.ImportDnsRecordsAsync(
targetZoneId,
bindContent,
proxied: enableProxy);
logger.LogInformation(
"Migration complete: {Added}/{Parsed} records imported",
result.RecordsAdded,
result.TotalRecordsParsed);
}
}
Discover Existing Records
Moving a domain that already has DNS elsewhere? Use the scan API to discover existing records (add to DnsMigrationService):
public async Task DiscoverExistingRecordsAsync(string zoneId)
{
logger.LogInformation("Scanning for existing DNS records...");
// Trigger async scan
await cloudflare.Dns.TriggerDnsRecordScanAsync(zoneId);
// Wait for scan to complete (in production, use proper polling)
await Task.Delay(TimeSpan.FromSeconds(10));
// Review discovered records
var discovered = await cloudflare.Dns.GetDnsRecordScanReviewAsync(zoneId);
logger.LogInformation("Discovered {Count} existing records", discovered.Count);
foreach (var record in discovered)
{
logger.LogInformation(" {Type} {Name} -> {Content}", record.Type, record.Name, record.Content);
}
// Accept all discovered records
if (discovered.Count > 0)
{
var review = new DnsScanReviewRequest
{
// Convert DnsRecord objects to DnsScanAcceptItem for the API
Accepts = discovered.Select(DnsScanAcceptItem.FromDnsRecord).ToList()
};
var result = await cloudflare.Dns.SubmitDnsRecordScanReviewAsync(zoneId, review);
logger.LogInformation("Accepted {Count} records", result.Accepts);
}
}
Installation
dotnet add package Cloudflare.NET.Api
Configuration
See the Getting Started guide for full setup instructions.
appsettings.json
{
"Cloudflare": {
"ApiToken": "your-api-token",
"AccountId": "your-account-id"
}
}
Required Permissions
| Scenario | Permission | Level |
|---|---|---|
| DNS Records | DNS: Edit | Zone |
| Custom Hostnames | SSL and Certificates: Edit | Zone |
| Custom Hostnames | Cloudflare for SaaS: Edit | Zone |
| Zone Import/Export | DNS: Edit | Zone |
Dependency Injection Setup
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCloudflareApiClient(builder.Configuration);
builder.Services.AddScoped<TenantProvisioningService>();
builder.Services.AddScoped<CustomDomainService>();
builder.Services.AddScoped<DnsMigrationService>();
Error Handling
The SDK provides structured exceptions for DNS and Custom Hostname operations:
try
{
await cloudflare.Dns.CreateDnsRecordAsync(zoneId, request);
}
catch (CloudflareApiException ex) when (ex.Errors.Any(e => e.Code == 81057))
{
// Record already exists
logger.LogWarning("DNS record already exists: {Message}", ex.Errors.First().Message);
}
catch (CloudflareApiException ex) when (ex.Errors.Any(e => e.Code == 81058))
{
// Invalid record content
logger.LogError("Invalid DNS record: {Message}", ex.Errors.First().Message);
}
catch (CloudflareApiException ex)
{
// Other API errors
foreach (var error in ex.Errors)
{
logger.LogError("[{Code}] {Message}", error.Code, error.Message);
}
}
Summary
Most teams have CI/CD, infrastructure-as-code, automated deployments... and then someone clicks around in the Cloudflare dashboard to add a DNS record. The Cloudflare.NET SDK fixes that:
| Scenario | What You Get |
|---|---|
| Multi-Tenant Subdomains | Automated provisioning tied to your signup flow |
| Custom Domains (Cloudflare for SaaS) | White-label SSL with programmatic DCV workflow |
| Zone Migration | BIND import/export for provider transitions |
| Record Discovery | Scan and review existing DNS before migration |
The batch API runs in a single transaction, so you can delete a record and recreate it with the same name without race conditions.
Get started:
dotnet add package Cloudflare.NET.Api
Learn more:
- GitHub Repository
- API Documentation
- DNS API Coverage
- Cloudflare R2 in .NET Without the AWS SDK Headaches - Companion article on R2 object storage
Cloudflare.NET is an open-source, community-maintained SDK. Contributions are welcome!
Top comments (0)