.NET MVC CORE

How to Upload Images from a .NET Backend to Cloudflare R2 and Store the Image URL

Modern applications often need to store user-uploaded images such as profile pictures, food photos, receipts, invoices, or product images. While storing files directly on your web server works initially, it becomes difficult to manage as your application grows. A better solution is to use cloud object storage. Cloudflare R2 is a cost-effective, scalable object storage service that is compatible with Amazon S3 APIs and provides global access to uploaded files.

J
Joynal Abedin
9
How to Upload Images from a .NET Backend to Cloudflare R2 and Store the Image URL

In this article, we will learn how to:

  • Upload images from a .NET backend to Cloudflare R2
  • Generate a public image URL
  • Store the image URL in a database
  • Return the image URL to mobile or web applications

By the end of this guide, your application architecture will look like this:

Mobile App / Web App ↓ .NET API ↓ Cloudflare R2 ↓ Public Image URL ↓ Database

Why Use Cloudflare R2?

Cloudflare R2 offers several advantages:

  • S3-compatible API
  • Lower storage costs
  • No egress fees in many scenarios
  • Global CDN integration
  • High scalability
  • Easy integration with .NET applications

Instead of storing images on your hosting server, you can store them in R2 and save only the image URL in your database.

Step 1: Create an R2 Bucket

Login to the Cloudflare Dashboard.

Navigate to: R2 Object Storage Create Bucket Example: Bucket Name: food-images

After creating the bucket, enable Public Access. Cloudflare will provide a public URL similar to: https://pub-xxxxxxxxxxxxxxxx.r2.dev

Keep this URL because it will be used to access uploaded images.

Step 2: Create an R2 API Token

Navigate to: R2 → Manage R2 API Tokens Create API Token Select: Object Read & Write

Cloudflare will generate:

  • Access Key ID
  • Secret Access Key
  • Endpoint URL

Example: Access Key: abc123

Secret Key: xyz456

Endpoint: https://123456789.r2.cloudflarestorage.com

Store these credentials securely.

Step 3: Install AWS S3 SDK

Cloudflare R2 is fully compatible with the AWS S3 SDK.

Install the package: dotnet add package AWSSDK.S3

Step 4: Configure Application Settings

Add Cloudflare configuration to appsettings.json. { "CloudflareR2": { "AccessKey": "YOUR_ACCESS_KEY", "SecretKey": "YOUR_SECRET_KEY", "ServiceUrl": "https://123456789.r2.cloudflarestorage.com", "BucketName": "food-images", "PublicUrl": "https://pub-xxxxxxxxxxxxxxxx.r2.dev" } }

Step 5: Create Configuration Model

Create a settings class.

public class CloudflareR2Settings
{
    public string AccessKey { get; set; }
    public string SecretKey { get; set; }
    public string ServiceUrl { get; set; }
    public string BucketName { get; set; }
    public string PublicUrl { get; set; }
}

Register the settings.

builder.Services.Configure<CloudflareR2Settings>(
    builder.Configuration.GetSection("CloudflareR2"));

Step 6: Create the Upload Service

Create a service responsible for uploading files to Cloudflare R2.

using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.Extensions.Options;

public class R2StorageService
{
    private readonly CloudflareR2Settings _settings;
    private readonly AmazonS3Client _client;

    public R2StorageService(
        IOptions<CloudflareR2Settings> options)
    {
        _settings = options.Value;

        var config = new AmazonS3Config
        {
            ServiceURL = _settings.ServiceUrl,
            ForcePathStyle = true
        };

        _client = new AmazonS3Client(
            _settings.AccessKey,
            _settings.SecretKey,
            config);
    }

    public async Task<string> UploadImageAsync(IFormFile file)
    {
        var extension = Path.GetExtension(file.FileName);

        var fileName =
            $"{Guid.NewGuid()}{extension}";

        using var stream = file.OpenReadStream();

        var request = new PutObjectRequest
        {
            BucketName = _settings.BucketName,
            Key = fileName,
            InputStream = stream,
            ContentType = file.ContentType
        };

        await _client.PutObjectAsync(request);

        return $"{_settings.PublicUrl}/{fileName}";
    }
}

Register the service:

builder.Services.AddScoped<R2StorageService>();

Step 7: Create an Upload API Endpoint

Create an API controller.

[ApiController]
[Route("api/images")]
public class ImagesController : ControllerBase
{
    private readonly R2StorageService _storage;

    public ImagesController(R2StorageService storage)
    {
        _storage = storage;
    }

    [HttpPost]
    public async Task<IActionResult> Upload(
        IFormFile image)
    {
        var imageUrl =
            await _storage.UploadImageAsync(image);

        return Ok(new
        {
            Url = imageUrl
        });
    }
}

This endpoint accepts an image file, uploads it to R2, and returns the public URL.


Step 8: Test the Upload

Open Postman.

Send:

POST /api/images

Body:

form-data

Key:

image

Type:

File

Select an image and send the request.

Example response:

{
  "url": "https://pub-xxxxxxxxxxxxxxxx.r2.dev/12345.jpg"
}

Open the URL in a browser to verify the upload.


Step 9: Save the URL to the Database

Instead of saving the image itself, save the image URL.

Example entity:

public class FoodLog
{
    public int Id { get; set; }

    public string ImageUrl { get; set; }

    public DateTime CreatedAt { get; set; }
}

Save after upload:

var imageUrl =
    await _storage.UploadImageAsync(image);

var foodLog = new FoodLog
{
    ImageUrl = imageUrl,
    CreatedAt = DateTime.UtcNow
};

_context.FoodLogs.Add(foodLog);

await _context.SaveChangesAsync();

Database record:

https://pub-xxxxxxxxxxxxxxxx.r2.dev/12345.jpg

Step 10: Display Images in the Mobile App

The backend returns:

{
  "url": "https://pub-xxxxxxxxxxxxxxxx.r2.dev/12345.jpg"
}

In SwiftUI:

AsyncImage(
    url: URL(string: imageUrl)
)

The image will be loaded directly from Cloudflare R2.


Organizing Uploaded Files

As your application grows, avoid storing all files in the bucket root.

Instead:

food-images/
 ├── foods/
 ├── profiles/
 ├── receipts/
 └── invoices/

Example:

var fileName =
    $"foods/{Guid.NewGuid()}.jpg";

Generated URL:

https://pub-xxxxxxxxxxxxxxxx.r2.dev/foods/abc123.jpg

This keeps storage organized and easier to maintain.


Conclusion

Using Cloudflare R2 with a .NET backend provides a secure and scalable solution for image storage. The recommended workflow is:

  1. User uploads an image from the mobile app.
  2. The .NET API receives the file.
  3. The API uploads the file to Cloudflare R2.
  4. Cloudflare returns a public image URL.
  5. The URL is stored in the database.
  6. The mobile application displays the image using the stored URL.

This approach reduces server storage usage, improves scalability, and makes image delivery faster through Cloudflare's infrastructure.

J

Written by Joynal Abedin

Passionate about technology, code, and sharing knowledge.

0 Comments

Leave a Comment