R2 Object Storage
The Cloudflare.NET.R2 package provides a high-level client for Cloudflare R2 object storage using the S3-compatible API.
Installation
dotnet add package Cloudflare.NET.R2
Configuration
{
"Cloudflare": {
"AccountId": "your-cloudflare-account-id"
},
"R2": {
"AccessKeyId": "your-r2-access-key-id",
"SecretAccessKey": "your-r2-secret-access-key"
}
}
builder.Services.AddCloudflareR2Client(builder.Configuration);
Jurisdiction Support
R2 supports jurisdictional restrictions that ensure data stays within specific geographic regions. The SDK provides full support for jurisdictions through configuration and the IR2ClientFactory.
Available Jurisdictions
| Jurisdiction | Description | Endpoint |
|---|---|---|
Default |
No restriction (global) | https://{account_id}.r2.cloudflarestorage.com |
EuropeanUnion |
EU data residency | https://{account_id}.eu.r2.cloudflarestorage.com |
FedRamp |
FedRAMP compliance | https://{account_id}.fedramp.r2.cloudflarestorage.com |
UnitedStates |
US data residency | https://{account_id}.us.r2.cloudflarestorage.com |
Configuration-Based Jurisdiction
Set the jurisdiction in your configuration to use it as the default:
{
"R2": {
"AccessKeyId": "...",
"SecretAccessKey": "...",
"Jurisdiction": "eu"
}
}
Runtime Jurisdiction with IR2ClientFactory
Use IR2ClientFactory to create clients for different jurisdictions at runtime:
public class MultiJurisdictionService(IR2ClientFactory factory)
{
// Get client for specific jurisdiction using default credentials
public async Task UploadToEuAsync(string bucket, string key, Stream data)
{
var euClient = factory.GetClient(R2Jurisdiction.EuropeanUnion);
await euClient.UploadAsync(bucket, key, data);
}
// Get client for specific jurisdiction using named credentials
public async Task UploadToProdEuAsync(string bucket, string key, Stream data)
{
var client = factory.GetClient("production", R2Jurisdiction.EuropeanUnion);
await client.UploadAsync(bucket, key, data);
}
}
Note
The same R2 credentials work across all jurisdictions within an account, only the S3 endpoint differs. Clients are cached by (name, jurisdiction) tuple and reused.
Named Clients
For multi-account scenarios, register named R2 clients:
// Registration
services.AddCloudflareR2Client("production", config);
services.AddCloudflareR2Client("staging", config);
// Usage
public class StorageService(IR2ClientFactory factory)
{
public IR2Client Production => factory.GetClient("production");
public IR2Client Staging => factory.GetClient("staging");
}
Basic Operations
Upload Objects
public class StorageService(IR2Client r2)
{
// Upload a file (auto-selects single or multipart based on size)
public async Task<R2Result> UploadFileAsync(string bucket, string key, string filePath)
{
return await r2.UploadAsync(bucket, key, filePath);
}
// Upload from stream
public async Task<R2Result> UploadStreamAsync(string bucket, string key, Stream stream)
{
return await r2.UploadAsync(bucket, key, stream);
}
// Upload as a multipart upload, recording the content type the finished object will carry.
// The upload methods above do not set a content type, so R2 stores those objects as
// application/octet-stream. Setting it requires the multipart route or a presigned PUT URL.
public async Task<R2Result> UploadWithContentTypeAsync(
string bucket,
string key,
string filePath,
string contentType)
{
var initiate = await r2.InitiateMultipartUploadAsync(bucket, key, contentType);
// Upload each part, then complete. See the multipart article for the full sequence.
return initiate.Metrics;
}
}
Note
UploadAsync, UploadSinglePartAsync and UploadMultipartAsync do not accept a content type, so
objects they write carry R2's default of application/octet-stream. Use
InitiateMultipartUploadAsync with a content type, or a presigned PUT URL whose
PresignedPutRequest.ContentType names the type, when the stored value matters.
Download Objects
// Download to file
public async Task DownloadToFileAsync(string bucket, string key, string filePath)
{
await r2.DownloadFileAsync(bucket, key, filePath);
}
// Download to stream
public async Task<Stream> DownloadToStreamAsync(string bucket, string key)
{
var memoryStream = new MemoryStream();
await r2.DownloadFileAsync(bucket, key, memoryStream);
memoryStream.Position = 0;
return memoryStream;
}
Delete Objects
// Delete single object (free operation)
public async Task DeleteObjectAsync(string bucket, string key)
{
await r2.DeleteObjectAsync(bucket, key);
}
// Batch delete (up to 1000 keys per request)
public async Task DeleteObjectsAsync(string bucket, IEnumerable<string> keys)
{
await r2.DeleteObjectsAsync(bucket, keys);
}
// Clear entire bucket: deletes every object, then aborts every multipart
// upload left open, since R2 will not delete a bucket while one is open.
public async Task ClearBucketAsync(string bucket)
{
await r2.ClearBucketAsync(bucket);
}
List Objects
// Walk every page and return the complete set of objects
public async Task<IReadOnlyList<S3Object>> ListAllAsync(string bucket, string? prefix = null)
{
var result = await r2.ListObjectsAsync(bucket, prefix);
return result.Data;
}
// Read one page at a time when the bucket is too large to hold in memory
public async Task ProcessPageByPageAsync(string bucket, string? prefix)
{
string? token = null;
do
{
var page = await r2.ListObjectsPageAsync(bucket, prefix, 1000, token);
foreach (var obj in page.Data.Objects)
{
await ProcessAsync(obj);
}
token = page.Data.NextContinuationToken;
}
while (token is not null);
}
// Find multipart uploads that were started and never completed.
// Their parts occupy storage that object listing cannot show you.
public async Task<IReadOnlyList<MultipartUpload>> FindOpenUploadsAsync(string bucket)
{
var result = await r2.ListMultipartUploadsAsync(bucket, null);
return result.Data;
}
Presigned URLs
Generate presigned URLs so clients can transfer bytes directly to and from R2:
// Generate presigned PUT URL for a direct upload
public string GetPresignedUploadUrl(string bucket, string key, long size, string contentType)
{
return r2.CreatePresignedPutUrl(bucket, new PresignedPutRequest(
Key: key,
ExpiresAfter: TimeSpan.FromHours(1),
ContentLength: size,
ContentType: contentType));
}
// Generate presigned GET URL for a direct download,
// naming the file the recipient's browser will save
public string GetPresignedDownloadUrl(string bucket, string key, string downloadName)
{
return r2.CreatePresignedGetUrl(bucket, new PresignedGetRequest(
Key: key,
ExpiresAfter: TimeSpan.FromMinutes(15),
ResponseContentDisposition: $"attachment; filename=\"{downloadName}\""));
}
// Generate presigned URL for one part of a multipart upload
public string GetPresignedPartUrl(
string bucket, string key, string uploadId, int partNumber, long partSize)
{
return r2.CreatePresignedUploadPartUrl(bucket, new PresignedUploadPartRequest(
Key: key,
UploadId: uploadId,
PartNumber: partNumber,
ExpiresAfter: TimeSpan.FromHours(1),
ContentLength: partSize,
ContentType: "application/octet-stream"));
}
Multipart Uploads
For large files (> 5 GiB), use multipart uploads:
// Automatic multipart upload based on size
public async Task<R2Result> UploadLargeFileAsync(string bucket, string key, string filePath)
{
// Automatically uses multipart for files > threshold
return await r2.UploadAsync(bucket, key, filePath);
}
// Explicit multipart upload
public async Task<R2Result> UploadMultipartAsync(string bucket, string key, Stream stream)
{
return await r2.UploadMultipartAsync(bucket, key, stream);
}
Operation Metrics
All R2 operations return R2Result or R2Result<T> which includes operation metrics:
var result = await r2.UploadAsync(bucket, key, stream);
Console.WriteLine($"Class A Operations: {result.ClassAOperations}");
Console.WriteLine($"Class B Operations: {result.ClassBOperations}");
Console.WriteLine($"Ingress Bytes: {result.IngressBytes}");
Console.WriteLine($"Egress Bytes: {result.EgressBytes}");
Error Handling
The R2 client provides specialized exceptions:
try
{
await r2.UploadAsync(bucket, key, stream);
}
catch (CloudflareR2OperationException ex)
{
// Single operation failure
Console.WriteLine($"Operation failed: {ex.Message}");
Console.WriteLine($"Partial metrics: {ex.PartialMetrics}");
}
catch (CloudflareR2BatchException<string> ex)
{
// Batch operation failure
foreach (var failedKey in ex.FailedItems)
{
Console.WriteLine($"Failed to delete: {failedKey}");
}
}
catch (CloudflareR2ConfigurationException ex)
{
// Missing or invalid configuration
Console.WriteLine($"Configuration error: {ex.Message}");
}
Bucket Management
Bucket operations are available through the REST API client:
public class BucketService(ICloudflareApiClient cf)
{
// Basic bucket creation
public async Task<R2Bucket> CreateBucketAsync(string name)
{
return await cf.Accounts.Buckets.CreateAsync(name);
}
// Create with location hint and jurisdiction
public async Task<R2Bucket> CreateEuBucketAsync(string name)
{
return await cf.Accounts.Buckets.CreateAsync(
name,
locationHint: R2LocationHint.WestEurope,
jurisdiction: R2Jurisdiction.EuropeanUnion
);
}
public async IAsyncEnumerable<R2Bucket> ListBucketsAsync()
{
await foreach (var bucket in cf.Accounts.Buckets.ListAllAsync())
{
// Access extensible enum properties with IntelliSense
if (bucket.Location == R2LocationHint.WestEurope)
{
Console.WriteLine($"EU bucket: {bucket.Name}");
}
yield return bucket;
}
}
public async Task DeleteBucketAsync(string name)
{
await cf.Accounts.Buckets.DeleteAsync(name);
}
}
The R2Bucket model uses extensible enums for Location, Jurisdiction, and StorageClass properties. See SDK Conventions for details.