Implement FIle Upload Service to Cloudflare R2 with C# and .NET
Implement a basic file upload service with methods to upload and delete single and multiple files
We'll be using the Options pattern so create the R2Options.cs file
public class R2Options
{
public string AccountId { get; set; } = string.Empty;
public string AccessKey { get; set; } = string.Empty;
public string SecretKey { get; set; } = string.Empty;
public string BucketName { get; set; } = string.Empty;
public string EndpointUrl { get; set; } = string.Empty;
public string Token { get; set; } = string.Empty;
}
Create a simple file upload model
public record FileUploadRequest(string Key, Stream FileStream, string ContentType);
I'll be using the functional Result pattern for error handling. You can find a sample implementation on this site at The Result Pattern in .NET
public partial class R2Service : IR2Service
{
private readonly ILogger<R2Service> _logger;
private readonly R2Options _options;
private readonly AmazonS3Client _s3Client;
public R2Service(IOptions<R2Options> options, ILogger<R2Service> logger)
{
_options = options.Value;
_logger = logger;
var credentials = new BasicAWSCredentials(_options.AccessKey, _options.SecretKey);
var config = new AmazonS3Config
{
ServiceURL = _options.EndpointUrl,
ForcePathStyle = true,
};
AWSConfigsS3.UseSignatureVersion4 = true;
_s3Client = new AmazonS3Client(credentials, config);
}
public async Task<Result> UploadFileAsync(FileUploadRequest file)
{
try
{
_logger.LogInformation("Uploading file {Key} to R2 bucket {BucketName}", file.Key, _options.BucketName);
var putRequest = new PutObjectRequest
{
BucketName = _options.BucketName,
Key = file.Key,
InputStream = file.FileStream,
ContentType = file.ContentType,
CannedACL = S3CannedACL.PublicRead,
DisablePayloadSigning = true,
DisableDefaultChecksumValidation = true
};
var response = await _s3Client.PutObjectAsync(putRequest);
if (response.HttpStatusCode == HttpStatusCode.OK)
{
_logger.LogInformation("File {Key} uploaded successfully to R2 bucket {BucketName}",
file.Key, _options.BucketName);
return Result.Success();
}
_logger.LogError("Failed to upload file {Key} to R2 bucket {BucketName}. Status code: {StatusCode}",
file.Key, _options.BucketName, response.HttpStatusCode);
return Result.Failure((int)response.HttpStatusCode, CloudErrors.FileUploadFailed);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occurred while uploading file {Key} to R2 bucket {BucketName}",
file.Key, _options.BucketName);
return Result.Failure(StatusCodes.Status500InternalServerError, CloudErrors.CloudOperationException);
}
finally
{
await file.FileStream.DisposeAsync();
}
}
public async Task<Result<List<string>>> UploadMultipleFilesAsync(IEnumerable<FileUploadRequest> files)
{
try
{
var uploadTasks = files.Select(UploadFileAsync);
var results = await Task.WhenAll(uploadTasks);
var failures = results.Where(r => !r.IsSuccess).ToList();
if (failures.Count != 0)
{
var failedKeys = files.Zip(results, (file, result) => new { file.Key, result })
.Where(x => !x.result.IsSuccess)
.Select(x => x.Key)
.ToList();
_logger.LogWarning("Failed to upload {Count} files: {FailedKeys}",
failures.Count, string.Join(", ", failedKeys));
return Result<List<string>>.Failure(StatusCodes.Status207MultiStatus,
CloudErrors.FileUploadFailed);
}
var uploadedKeys = files.Select(f => f.Key).ToList();
_logger.LogInformation("Successfully uploaded {Count} files", uploadedKeys.Count);
return Result<List<string>>.Success(uploadedKeys);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occurred while uploading multiple files");
return Result<List<string>>.Failure(StatusCodes.Status500InternalServerError,
CloudErrors.CloudOperationException);
}
}
public async Task<Result> DeleteFileAsync(string key)
{
try
{
_logger.LogInformation("Deleting file {Key} from R2 bucket {BucketName}", key, _options.BucketName);
var deleteRequest = new DeleteObjectRequest
{
BucketName = _options.BucketName,
Key = key,
};
var response = await _s3Client.DeleteObjectAsync(deleteRequest);
if (response.HttpStatusCode == HttpStatusCode.NoContent)
{
_logger.LogInformation("File {Key} deleted successfully from R2 bucket {BucketName}",
key, _options.BucketName);
return Result.Success();
}
_logger.LogError("Failed to delete file {Key} from R2 bucket {BucketName}. Status code: {StatusCode}",
key, _options.BucketName, response.HttpStatusCode);
return Result.Failure((int)response.HttpStatusCode, CloudErrors.FileDeletionFailed);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occurred while deleting file {Key} from R2 bucket {BucketName}",
key, _options.BucketName);
return Result.Failure(StatusCodes.Status500InternalServerError, CloudErrors.CloudOperationException);
}
}
public async Task<Result<List<string>>> DeleteMultipleFilesAsync(IEnumerable<string> keys)
{
try
{
var keyList = keys.ToList();
if (keyList.Count == 0)
{
return Result<List<string>>.Failure(StatusCodes.Status400BadRequest, CloudErrors.NoKeysProvided);
}
_logger.LogInformation("Deleting {Count} files from R2 bucket {BucketName}",
keyList.Count, _options.BucketName);
var deleteRequest = new DeleteObjectsRequest
{
BucketName = _options.BucketName,
Objects = [.. keyList.Select(key => new KeyVersion { Key = key })]
};
var response = await _s3Client.DeleteObjectsAsync(deleteRequest);
if (response.HttpStatusCode == HttpStatusCode.OK)
{
var deletedKeys = response.DeletedObjects.Select(obj => obj.Key).ToList();
var failedKeys = response.DeleteErrors.Select(error => error.Key).ToList();
if (failedKeys.Count != 0)
{
_logger.LogWarning("Failed to delete {Count} files: {FailedKeys}",
failedKeys.Count, string.Join(", ", failedKeys));
return Result<List<string>>.Failure(StatusCodes.Status207MultiStatus,
CloudErrors.FileDeletionFailed);
}
_logger.LogInformation("Successfully deleted {Count} files from R2 bucket {BucketName}",
deletedKeys.Count, _options.BucketName);
return Result<List<string>>.Success(deletedKeys);
}
_logger.LogError("Failed to delete files from R2 bucket {BucketName}. Status code: {StatusCode}",
_options.BucketName, response.HttpStatusCode);
return Result<List<string>>.Failure((int)response.HttpStatusCode, CloudErrors.FileDeletionFailed);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occurred while deleting multiple files from R2 bucket {BucketName}",
_options.BucketName);
return Result<List<string>>.Failure(StatusCodes.Status500InternalServerError,
CloudErrors.CloudOperationException);
}
}
}
Configure the cloudflare R2 options and register the service for dependency injection
services.Configure<R2Options>(configuration.GetSection("Cloudflare:R2"));
services.AddScoped<IR2Service, R2Service>();
And then in your appsettings.json file (or wherever you deem appropriate), add the cloudflare r2 configurations
"Cloudflare": {
"R2": {
"AccountId": "your-account-id",
"AccessKey": "your-access-key",
"SecretKey": "your-secret-key",
"BucketName": "your-bucket-name",
"EndpointUrl": "https://your-account-id.r2.cloudflarestorage.com",
"Token": "your-token"
}
}
Enjoy!