Implement Blob Storage Using Presigned URL in .NET Core and JavaScript


Uploading files through your backend server can create performance bottlenecks, especially for large files like images, videos, PDFs, or backups.

A better and scalable solution is using Presigned URLs.

With Presigned URLs:

  • Backend generates a temporary upload URL
  • Frontend uploads directly to blob storage
  • Backend never handles file data
  • Uploads become faster and more secure

In this blog, we will implement:

  • ASP.NET Core Web API
  • AWS S3 Blob Storage
  • JavaScript Frontend
  • Secure Upload using Presigned URL

What is a Presigned URL?

A Presigned URL is a temporary secure URL generated by the backend that allows clients to upload files directly to cloud storage.

The URL:

  • Has expiration time
  • Works for a specific file
  • Prevents exposing cloud credentials

Architecture

Frontend (JavaScript)
        |
        | Request Upload URL
        v
ASP.NET Core Backend
        |
        | Generate Presigned URL
        v
AWS S3
        ^
        |
        | Direct Upload
        |
Frontend

Benefits

  • Faster Uploads
    • Files go directly to storage.
  • Reduced Backend Load
    • Backend handles authentication only.
  • Better Scalability
    • Ideal for large applications.
  • Improved Security
    • URLs expire automatically.

Technologies Used

Step 1 — Create S3 Bucket

Create an S3 bucket from AWS Console.

Example bucket:

my-dotnet-upload-demo

Step 2 — Configure CORS in S3

Bucket → Permissions → CORS

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["PUT", "GET"],
    "AllowedOrigins": ["*"],
    "ExposeHeaders": []
  }
]

For production, replace "*" with your frontend domain.

Step 3 — Create ASP.NET Core Web API

Create project:

dotnet new webapi -n FileUploadApi

Move into project:

cd FileUploadApi

Step 4 — Install AWS SDK

Install package:

dotnet add package AWSSDK.S3

Step 5 — Configure appsettings.json

appsettings.json

{
  "AWS": {
    "AccessKey": "YOUR_ACCESS_KEY",
    "SecretKey": "YOUR_SECRET_KEY",
    "Region": "ap-south-1",
    "BucketName": "my-dotnet-upload-demo"
  }
}

Step 6 — Create Request Model

Models/UploadRequest.cs

namespace FileUploadApi.Models
{
    public class UploadRequest
    {
        public string FileName { get; set; }
        public string FileType { get; set; }
    }
}

Step 7 — Create Upload Controller

Controllers/UploadController.cs

using Amazon;
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.AspNetCore.Mvc;
using FileUploadApi.Models;

namespace FileUploadApi.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class UploadController : ControllerBase
    {
        private readonly IConfiguration _configuration;

        public UploadController(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        [HttpPost("generate-url")]
        public IActionResult GenerateUploadUrl([FromBody] UploadRequest request)
        {
            var accessKey = _configuration["AWS:AccessKey"];
            var secretKey = _configuration["AWS:SecretKey"];
            var region = _configuration["AWS:Region"];
            var bucketName = _configuration["AWS:BucketName"];

            var s3Client = new AmazonS3Client(
                accessKey,
                secretKey,
                RegionEndpoint.GetBySystemName(region)
            );

            var fileKey = $"uploads/{Guid.NewGuid()}-{request.FileName}";

            var presignedRequest = new GetPreSignedUrlRequest
            {
                BucketName = bucketName,
                Key = fileKey,
                Verb = HttpVerb.PUT,
                Expires = DateTime.UtcNow.AddMinutes(1),
                ContentType = request.FileType
            };

            string uploadUrl = s3Client.GetPreSignedURL(presignedRequest);

            var fileUrl =
                $"https://{bucketName}.s3.{region}.amazonaws.com/{fileKey}";

            return Ok(new
            {
                UploadUrl = uploadUrl,
                FileUrl = fileUrl
            });
        }
    }
}

Step 8 — Enable CORS in ASP.NET Core

Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll",
        policy =>
        {
            policy.AllowAnyOrigin()
                  .AllowAnyMethod()
                  .AllowAnyHeader();
        });
});

var app = builder.Build();

app.UseCors("AllowAll");

app.MapControllers();

app.Run();

Step 9 — Run Backend

dotnet run

Backend runs at:

https://localhost:5001

Step 10 — Frontend HTML

index.html

<!DOCTYPE html>
<html>
<head>
    <title>File Upload</title>
</head>
<body>

    <h2>Upload File Using Presigned URL</h2>

    <input type="file" id="fileInput" />
    <button onclick="uploadFile()">Upload</button>

    <script src="app.js"></script>

</body>
</html>

Step 11 — Frontend JavaScript

app.js

async function uploadFile() {

    const fileInput = document.getElementById("fileInput");

    if (!fileInput.files.length) {
        alert("Please select a file");
        return;
    }

    const file = fileInput.files[0];

    try {

        // Step 1: Get Presigned URL
        const response = await fetch(
            "https://localhost:5001/api/upload/generate-url",
            {
                method: "POST",
                headers: {
                    "Content-Type": "application/json"
                },
                body: JSON.stringify({
                    fileName: file.name,
                    fileType: file.type
                })
            }
        );

        const data = await response.json();

        // Step 2: Upload directly to S3
        const uploadResponse = await fetch(data.uploadUrl, {
            method: "PUT",
            headers: {
                "Content-Type": file.type
            },
            body: file
        });

        if (uploadResponse.ok) {

            alert("File uploaded successfully!");

            console.log("File URL:", data.fileUrl);

        } else {

            alert("Upload failed");

        }

    } catch (error) {

        console.error(error);

        alert("Something went wrong");
    }
}

How the Flow Works

Step 1

Frontend requests upload URL:

POST /api/upload/generate-url

Step 2

Backend generates secure temporary URL:

s3Client.GetPreSignedURL(presignedRequest);

Step 3

Frontend uploads file directly:

fetch(uploadUrl, {
    method: "PUT",
    body: file
})

Validate File Types

Add validation inside controller:

var allowedTypes = new[]
{
    "image/png",
    "image/jpeg",
    "application/pdf"
};

if (!allowedTypes.Contains(request.FileType))
{
    return BadRequest("Invalid file type");
}

Limit File Size

Frontend validation:

const maxSize = 5 * 1024 * 1024;

if (file.size > maxSize) {
    alert("File size exceeds limit");
    return;
}

Recommended Security Practices

Never Expose AWS Credentials

Keep credentials only on backend.

Use Short Expiry

Expires = DateTime.UtcNow.AddMinutes(1)

Use IAM Restricted Permissions

Only allow:

{
  "Effect": "Allow",
  "Action": ["s3:PutObject"],
  "Resource": "arn:aws:s3:::my-dotnet-upload-demo/*"
}

Common Errors

CORS Error

Fix:

  • Configure S3 CORS correctly
  • Enable backend CORS

SignatureDoesNotMatch

Fix:

  • Verify region
  • Verify bucket name
  • Check credentials

AccessDenied

  • Fix IAM policy permissions.

Production Improvements

Add Authentication

Allow only logged-in users to generate upload URLs.

Store Metadata in Database

Store:

  • File URL
  • User ID
  • Upload date
  • File type

Use CloudFront CDN

Improve global file delivery performance.

Final Thoughts

Using Presigned URLs with ASP.NET Core is one of the best ways to build scalable and secure file upload systems.

Benefits include:

  • Faster uploads
  • Lower server load
  • Better scalability
  • Improved security

This architecture is widely used in:

  • SaaS applications
  • Social media platforms
  • AI apps
  • Video platforms
  • Document systems

Official Documentation

0 Comments Report