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

ebeeraheem

View Profile
142 views
Sep 21, 2025

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!