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.
- β Faster uploads
- β Lower server memory usage
- β Reduced bandwidth cost
- β Highly scalable architecture
π Upload Flow
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.
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:
β 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:
Log in using your Cloudflare account.
π¦ Step 2: Open R2 Object Storage
From the left sidebar menu:
If you havenβt created a bucket yet, create one first.
π Step 3: Create R2 API Token
Inside R2 dashboard:
Then click:
βοΈ Step 4: Configure Permissions
Select permissions carefully:
β 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:
Secret Access Key: aBcD1234EXAMPLE56789
π§© 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 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:
- Login to Cloudflare
- Go to R2 Object Storage
- Manage R2 API Tokens
- Create Token with Read/Write permission
- Copy Access Key & Secret Key
Now your backend can securely connect to Cloudflare R2 and generate pre-signed URLs or upload files.