DEV Community

Alexis
Alexis

Posted on

Cloudflare R2 in .NET Without the AWS SDK Headaches

TL;DR: The Cloudflare.NET SDK provides IR2Client for R2 object storage with strongly-typed APIs, native dependency injection, automatic multipart uploads, and built-in operation metrics.


R2 is Cloudflare's object storage with zero egress fees. Unlike AWS S3 or Azure Blob Storage, you don't pay to download your own data.

While R2 offers S3 API compatibility and .NET developers can use the AWS SDK, the Cloudflare.NET SDK provides a streamlined .NET experience with IntelliSense, dependency injection, and automatic handling of R2-specific behaviors.


What the SDK Handles for You

When working with R2's S3-compatible API directly, there are a few things you need to know. The SDK handles these automatically:

Payload Signing Configuration

R2 requires specific S3 client settings. The official Cloudflare documentation specifies that payload signing must be disabled:

var request = new PutObjectRequest
{
    BucketName = bucketName,
    Key = objectKey,
    InputStream = stream,
    DisablePayloadSigning = true,
    DisableDefaultChecksumValidation = true
};
Enter fullscreen mode Exit fullscreen mode

The SDK configures this automatically, so you never need to remember these flags.

Multipart Upload Rules

R2 has specific rules for multipart uploads:

Constraint Value
Minimum part size 5 MiB (except the last part)
Maximum part size 5 GiB
Maximum total size 5 TiB
Maximum parts 10,000

The key rules to be aware of:

  1. All parts except the last must be at least 5 MiB.
  2. All parts except the last must be the same size. R2 requires uniform part sizes.
  3. Only the last part can be smaller than 5 MiB.
  4. The last part cannot be larger than the other parts.

The SDK validates these constraints automatically and calculates optimal part sizes for you.

Endpoint and Region Configuration

R2 uses a specific endpoint format and the region value "auto":

var config = new AmazonS3Config
{
    ServiceURL = $"https://{accountId}.r2.cloudflarestorage.com",
    ForcePathStyle = true,
    AuthenticationRegion = "auto"
};
Enter fullscreen mode Exit fullscreen mode

The SDK constructs this from your configuration automatically.

Operation Metrics

R2 has a specific pricing model based on operation classes:

  • Class A operations (writes, lists): $4.50 per million
  • Class B operations (reads): $0.36 per million
  • Delete operations: Free

The SDK tracks these automatically. Every operation returns an R2Result with operation counts and bytes transferred.


Key Features

Here's what the SDK provides:

Strongly-Typed APIs

Full IntelliSense with XML documentation. Enums and typed models guide you through the API:

var result = await r2.UploadAsync(
    bucketName: "my-bucket",
    objectKey: "path/to/file",
    filePath: "/local/file.pdf",
    partSize: 50 * 1024 * 1024);
Enter fullscreen mode Exit fullscreen mode

Simplified Configuration

Native dependency injection with appsettings.json:

builder.Services.AddCloudflareR2Client(builder.Configuration);
Enter fullscreen mode Exit fullscreen mode

Production-Ready Resilience

The core API client includes automatic retries with exponential backoff, circuit breaker, and rate limiting via Polly:

builder.Services.AddCloudflareApiClient(builder.Configuration);
Enter fullscreen mode Exit fullscreen mode

Operation Metrics

Every operation returns Class A/B counts and bytes transferred for cost monitoring:

var result = await r2.UploadAsync(bucket, key, filePath);

Console.WriteLine($"Class A ops: {result.ClassAOperations}");
Console.WriteLine($"Ingress: {result.IngressBytes} bytes");
Enter fullscreen mode Exit fullscreen mode

Additional Features

  • Smart upload selection: Auto-chooses single-part vs. multipart based on file size
  • Multipart validation: Enforces R2's part size rules before API calls
  • Typed exceptions: Clear error information with partial metrics
  • Presigned URLs: Generate secure URLs for direct browser uploads

Installation

dotnet add package Cloudflare.NET.Api
dotnet add package Cloudflare.NET.R2
Enter fullscreen mode Exit fullscreen mode

Configuration

appsettings.json

{
  "Cloudflare": {
    "AccountId": "your-cloudflare-account-id"
  },
  "R2": {
    "AccessKeyId": "your-r2-access-key-id",
    "SecretAccessKey": "your-r2-secret-access-key"
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Security Note: Never commit secrets to source control. Use User Secrets for development and environment variables or Azure Key Vault for production.

Dependency Injection Setup

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCloudflareApiClient(builder.Configuration);
builder.Services.AddCloudflareR2Client(builder.Configuration);

var app = builder.Build();
Enter fullscreen mode Exit fullscreen mode

Basic Operations

Uploading Files

public class StorageService(IR2Client r2)
{
    public async Task<R2Result> UploadAsync(string bucket, string key, string filePath)
    {
        return await r2.UploadAsync(bucket, key, filePath);
    }
}
Enter fullscreen mode Exit fullscreen mode

Upload from a stream:

await using var stream = File.OpenRead(filePath);
await r2.UploadAsync(bucket, key, stream);
Enter fullscreen mode Exit fullscreen mode

Downloading Files

// To file
await r2.DownloadFileAsync(bucket, key, destinationPath);

// To stream
await r2.DownloadFileAsync(bucket, key, outputStream);
Enter fullscreen mode Exit fullscreen mode

Deleting Objects

// Single delete (free)
await r2.DeleteObjectAsync(bucket, "documents/old-file.pdf");

// Batch delete (up to 1,000)
await r2.DeleteObjectsAsync(bucket, new[] { "temp/file1.txt", "temp/file2.txt" });

// Clear bucket
await r2.ClearBucketAsync(bucket);
Enter fullscreen mode Exit fullscreen mode

Multipart Uploads

The SDK automatically handles multipart uploads based on file size. You don't need to think about part sizing, R2's uniform-size requirement, or cleanup on failure.

// Upload a 2 GB file. Multipart happens automatically.
var result = await r2.UploadAsync(bucket, "backups/large-database.bak", filePath);
Enter fullscreen mode Exit fullscreen mode

Custom Part Size

Force multipart and specify part size (clamped to 5 MiB - 5 GiB):

await r2.UploadMultipartAsync(bucket, key, filePath, partSize: 100 * 1024 * 1024);
Enter fullscreen mode Exit fullscreen mode

Automatic Abort on Failure

If a multipart upload fails, the SDK automatically aborts to clean up orphaned parts:

try
{
    await r2.UploadMultipartAsync(bucket, key, filePath);
}
catch (CloudflareR2OperationException ex)
{
    Console.WriteLine($"Upload failed: {ex.Message}");
    Console.WriteLine($"Partial metrics: {ex.PartialMetrics}");
}
Enter fullscreen mode Exit fullscreen mode

Presigned URLs

Generate presigned URLs for direct client uploads:

var url = r2.CreatePresignedPutUrl(bucket, new PresignedPutRequest(
    Key: "uploads/document.pdf",
    ExpiresAfter: TimeSpan.FromMinutes(15),
    ContentLength: 10 * 1024 * 1024,
    ContentType: "application/pdf"
));
Enter fullscreen mode Exit fullscreen mode

Multipart Presigned URLs

For large client-side uploads, generate URLs for each part:

// Initiate multipart upload
var initResult = await r2.InitiateMultipartUploadAsync(bucket, key);
var uploadId = initResult.Data;

// Generate URLs for all parts
var urls = r2.CreatePresignedUploadPartsUrls(bucket, new PresignedUploadPartsRequest(
    Key: key,
    UploadId: uploadId,
    PartNumbers: Enumerable.Range(1, partCount).ToArray(),
    ExpiresAfter: TimeSpan.FromHours(1),
    ContentLength: partSize,
    ContentType: "application/octet-stream"
));
Enter fullscreen mode Exit fullscreen mode

The SDK validates part sizes before generating URLs, catching R2's uniform-size requirement early rather than after clients attempt uploads.


Error Handling

The SDK provides typed exceptions with partial metrics:

try
{
    await r2.UploadAsync(bucket, key, filePath);
}
catch (CloudflareR2OperationException ex)
{
    Console.WriteLine($"Failed: {ex.Message}");
    Console.WriteLine($"Partial metrics: {ex.PartialMetrics}");
}
catch (CloudflareR2BatchException<string> ex)
{
    Console.WriteLine($"Failed keys: {string.Join(", ", ex.FailedItems)}");
}
Enter fullscreen mode Exit fullscreen mode

Listing Objects

var result = await r2.ListObjectsAsync(bucket, prefix: "documents/");

foreach (var obj in result.Data)
{
    Console.WriteLine($"{obj.Key}: {obj.Size} bytes");
}
Enter fullscreen mode Exit fullscreen mode

The SDK handles continuation tokens automatically.


Complete Example: File Upload API

A minimal file upload API using ASP.NET Core:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCloudflareApiClient(builder.Configuration);
builder.Services.AddCloudflareR2Client(builder.Configuration);

var app = builder.Build();

app.MapPost("/upload/{bucket}/{*key}", async (
    string bucket, string key, IFormFile file, IR2Client r2) =>
{
    await using var stream = file.OpenReadStream();
    var result = await r2.UploadAsync(bucket, key, stream);
    return Results.Ok(new { key, result.IngressBytes, result.ClassAOperations });
});

app.MapGet("/download/{bucket}/{*key}", async (
    string bucket, string key, IR2Client r2, HttpResponse response) =>
{
    response.ContentType = "application/octet-stream";
    await r2.DownloadFileAsync(bucket, key, response.Body);
});

app.MapDelete("/{bucket}/{*key}", async (
    string bucket, string key, IR2Client r2) =>
{
    await r2.DeleteObjectAsync(bucket, key);
    return Results.NoContent();
});

app.Run();
Enter fullscreen mode Exit fullscreen mode

Multi-Account Support

Register named clients for multiple accounts:

builder.Services.AddCloudflareR2Client("production", options =>
{
    options.AccessKeyId = "prod-access-key";
    options.SecretAccessKey = "prod-secret-key";
});

builder.Services.AddCloudflareR2Client("staging", options =>
{
    options.AccessKeyId = "staging-access-key";
    options.SecretAccessKey = "staging-secret-key";
});
Enter fullscreen mode Exit fullscreen mode

Use via factory or keyed services (.NET 8+):

// Factory pattern
public class MigrationService(IR2ClientFactory factory)
{
    public async Task MigrateAsync(string bucket, string key)
    {
        var staging = factory.CreateClient("staging");
        var production = factory.CreateClient("production");

        using var stream = new MemoryStream();
        await staging.DownloadFileAsync(bucket, key, stream);
        stream.Position = 0;
        await production.UploadAsync(bucket, key, stream);
    }
}

// Keyed services
public class ProductionService([FromKeyedServices("production")] IR2Client r2)
{
    public async Task UploadAsync(string bucket, string key, Stream data)
        => await r2.UploadAsync(bucket, key, data);
}
Enter fullscreen mode Exit fullscreen mode

Bucket Management

The Cloudflare.NET.Api package provides bucket operations via the REST API for end-to-end workflows:

// Create a bucket with location hint
var bucket = await cf.Accounts.Buckets.CreateAsync("my-bucket", R2LocationHint.WestEurope);

// Get bucket details
var info = await cf.Accounts.Buckets.GetAsync("my-bucket");

// List all buckets
await foreach (var b in cf.Accounts.Buckets.ListAllAsync())
{
    Console.WriteLine($"{b.Name} - {b.Location}");
}

// Delete bucket (must be empty)
await r2.ClearBucketAsync("my-bucket");
await cf.Accounts.Buckets.DeleteAsync("my-bucket");
Enter fullscreen mode Exit fullscreen mode

This enables complete infrastructure-as-code scenarios where you provision buckets, manage objects, and tear down resources programmatically.


Summary

The Cloudflare.NET SDK provides a streamlined R2 experience for .NET developers:

Feature What You Get
Strongly-Typed APIs Full IntelliSense with XML documentation
Simplified Configuration One-line DI registration from appsettings.json
Production-Ready Resilience Automatic retries, circuit breaker, and rate limiting
Operation Metrics Class A/B counts and bytes transferred
Automatic Multipart Part sizing, validation, and orchestration
Bucket Management Create, list, and delete buckets via REST API

Get started:

dotnet add package Cloudflare.NET.Api
dotnet add package Cloudflare.NET.R2
Enter fullscreen mode Exit fullscreen mode

Learn more:


Cloudflare.NET is an open-source, community-maintained SDK. Contributions are welcome!

Top comments (0)