CloudFlare R2 Storage Service

Published At 2026/Feb/27
No Image Found

Upload 1–5 Images to Cloudflare R2 Using Pre-Signed URL in ASP.NET Core

A Production-Ready Guide Using Web API, ActionResult & Razor (.cshtml)


πŸš€ Why Use Pre-Signed URLs?

Instead of uploading files through your ASP.NET Core server, you can generate a temporary secure URL and allow the browser to upload directly to Cloudflare R2.

Benefits:
  • βœ… Faster uploads
  • βœ… Lower server memory usage
  • βœ… Reduced bandwidth cost
  • βœ… Highly scalable architecture

πŸ— Upload Flow

1️⃣ User selects 1–5 images
2️⃣ Client requests pre-signed URLs
3️⃣ Server generates temporary upload URLs
4️⃣ Browser uploads directly to R2
5️⃣ Client sends image keys to API
6️⃣ Server saves metadata in database

πŸ“¦ Install AWS SDK

dotnet add package AWSSDK.S3
        

βš™οΈ appsettings.json Configuration

{
  "CloudflareR2": {
    "AccessKey": "YOUR_ACCESS_KEY",
    "SecretKey": "YOUR_SECRET_KEY",
    "Endpoint": "https://your-account-id.r2.cloudflarestorage.com",
    "BucketName": "your-bucket-name",
    "PublicBaseUrl": "https://pub-xxxxxx.r2.dev"
  }
}
        

🧠 R2 Service (Generate Pre-Signed URL)

public string GeneratePresignedUploadUrl(string key, string contentType)
{
    var request = new GetPreSignedUrlRequest
    {
        BucketName = _config["CloudflareR2:BucketName"],
        Key = key,
        Verb = HttpVerb.PUT,
        Expires = DateTime.UtcNow.AddMinutes(10),
        ContentType = contentType
    };

    return _s3Client.GetPreSignedURL(request);
}
        

🌐 API: Generate Upload URLs (Max 5)

[HttpPost]
public IActionResult GenerateUploadUrls([FromBody] int fileCount)
{
    if (fileCount < 1 || fileCount > 5)
        return BadRequest("You can upload 1 to 5 images only.");

    var result = new List<object>();

    for (int i = 0; i < fileCount; i++)
    {
        var key = $"products/{Guid.NewGuid()}.jpg";
        var url = _r2Service.GeneratePresignedUploadUrl(key, "image/jpeg");

        result.Add(new
        {
            Key = key,
            UploadUrl = url,
            PublicUrl = $"{_config["CloudflareR2:PublicBaseUrl"]}/{key}"
        });
    }

    return Ok(result);
}
        

πŸ’Ύ Save Image Metadata

[HttpPost]
public async Task<IActionResult> SaveProductImages(
    int productId,
    [FromBody] List<string> keys)
{
    if (keys.Count > 5)
        return BadRequest("Maximum 5 images allowed.");

    var product = await _context.Products
        .Include(p => p.Images)
        .FirstOrDefaultAsync(p => p.Id == productId);

    if (product == null)
        return NotFound();

    foreach (var key in keys)
    {
        product.Images.Add(new ProductImage
        {
            R2Key = key,
            ImageUrl = $"{_config["CloudflareR2:PublicBaseUrl"]}/{key}"
        });
    }

    await _context.SaveChangesAsync();

    return Ok("Images saved successfully");
}
        

🎨 Frontend Upload (Inline Styled)

<input type="file" id="images" multiple />

<script>
async function uploadImages() {

    const files = document.getElementById("images").files;

    if (files.length < 1 || files.length > 5) {
        alert("Upload 1-5 images only.");
        return;
    }

    const response = await fetch('/api/Product/GenerateUploadUrls', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(files.length)
    });

    const presignedData = await response.json();
    let uploadedKeys = [];

    for (let i = 0; i < files.length; i++) {
        await fetch(presignedData[i].uploadUrl, {
            method: "PUT",
            headers: { "Content-Type": files[i].type },
            body: files[i]
        });

        uploadedKeys.push(presignedData[i].key);
    }

    await fetch('/api/Product/SaveProductImages?productId=1', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(uploadedKeys)
    });

    alert("Upload completed!");
}
</script>
        

πŸ–Ό Display Images in Razor

<div style="display:flex;flex-wrap:wrap;gap:15px;">
    @foreach (var img in Model.Images)
    {
        <div style="width:200px;height:200px;overflow:hidden;border-radius:8px;">
            <img src="@img.ImageUrl"
                 style="width:100%;height:100%;object-fit:cover;" />
        </div>
    }
</div>
        

πŸ”₯ Final Result

You now have a scalable ASP.NET Core system where users can upload 1–5 images directly to Cloudflare R2 using pre-signed URLs, store metadata in your database, and render images in Razor views.

Production Tip: Always validate file type, limit file size, and store the R2Key so you can delete or replace images later.

How to Get Cloudflare R2 Access Key & Secret Key

Step-by-step guide to generate S3 API credentials for Cloudflare R2


πŸ“Œ Why Do You Need Access Keys?

When connecting your ASP.NET Core or any backend application to Cloudflare R2, you need S3-compatible credentials:

βœ… Access Key ID
βœ… Secret Access Key

These allow your server to securely upload files, generate pre-signed URLs, and manage objects inside your R2 bucket.


πŸš€ Step 1: Login to Cloudflare Dashboard

Go to:

https://dash.cloudflare.com

Log in using your Cloudflare account.


πŸ“¦ Step 2: Open R2 Object Storage

From the left sidebar menu:

Click R2 β†’ Object Storage

If you haven’t created a bucket yet, create one first.


πŸ”‘ Step 3: Create R2 API Token

Inside R2 dashboard:

Click "Manage R2 API Tokens"

Then click:

Click "Create API Token"

βš™οΈ Step 4: Configure Permissions

Select permissions carefully:

βœ… Object Read
βœ… Object Write

You can allow access to:

  • All buckets
  • Or a specific bucket (recommended for security)

After selecting permissions, click Create Token.


πŸŽ‰ Step 5: Copy Your Keys

After creation, Cloudflare will show:

Access Key ID: 2JXKEXAMPLE123456
Secret Access Key: aBcD1234EXAMPLE56789
⚠️ Important: The Secret Key is shown only once. Save it securely before leaving the page.

🧩 How to Use in appsettings.json

"CloudflareR2": {
  "AccessKey": "YOUR_ACCESS_KEY",
  "SecretKey": "YOUR_SECRET_KEY",
  "Endpoint": "https://your-account-id.r2.cloudflarestorage.com",
  "BucketName": "your-bucket-name",
  "PublicBaseUrl": "https://pub-xxxxxx.r2.dev"
}
        

πŸ” Security Best Practices

βœ… Never expose Access Key in frontend JavaScript
βœ… Never commit Secret Key to GitHub
βœ… Use environment variables in production
βœ… Restrict token access to specific bucket only

🏁 Final Summary

Generating R2 Access Keys is simple:

  1. Login to Cloudflare
  2. Go to R2 Object Storage
  3. Manage R2 API Tokens
  4. Create Token with Read/Write permission
  5. Copy Access Key & Secret Key

Now your backend can securely connect to Cloudflare R2 and generate pre-signed URLs or upload files.