How Do I Handle File Uploads in ASP.NET Core with Azure Blob Storage?


I’m adding file uploads to an ASP.NET Core application and want the files stored in Azure Blob Storage rather than on the web server. What should the upload flow look like, and what details matter once it’s running in Azure?

A useful starting point is to keep the application responsible for validating the request and choosing a storage location, while Blob Storage holds the file itself. That keeps uploads out of your application’s local disk, which may be temporary or shared unpredictably when the app runs across multiple instances.

Give the app access without putting keys in code

For a deployed application, use a managed identity and grant it the Storage Blob Data Contributor role on the storage account or the specific scope it needs. In the application, construct the client with DefaultAzureCredential. During local development, that credential can use your signed-in Azure developer account; in Azure, it can use the app’s managed identity.

Configure the Blob service URI through application settings, then register a BlobServiceClient and the upload container as services in dependency injection. Keep the container private unless there is a clear reason for anonymous access. A private blob container prevents someone who guesses a URL from reading a file directly.

Stream the request to Blob Storage

For modest uploads, an ASP.NET Core controller can pass the incoming stream to the Azure Storage SDK. This avoids first copying the entire file into a byte array. The example assumes _container is an injected BlobContainerClient and that the container has already been provisioned.

[HttpPost]
[RequestSizeLimit(10_000_000)]
public async Task<IActionResult> Upload(
    [FromForm] IFormFile? file,
    CancellationToken cancellationToken)
{
    // Reject missing and empty multipart form files before contacting storage.
    if (file is null || file.Length == 0)
    {
        return BadRequest("Choose a non-empty file.");
    }

    // Apply an application-level limit as well as the request size limit.
    if (file.Length > 10_000_000)
    {
        return BadRequest("The file is too large.");
    }

    // Use a generated name so client-supplied filenames cannot overwrite another upload.
    var blobName = Guid.NewGuid().ToString("N");
    var blob = _container.GetBlobClient(blobName);

    // Pass the request stream through instead of buffering the whole file in memory.
    await using var stream = file.OpenReadStream();

    // Set a content type for later downloads; validate it separately before trusting it.
    await blob.UploadAsync(
        stream,
        new BlobUploadOptions
        {
            HttpHeaders = new BlobHttpHeaders
            {
                ContentType = file.ContentType
            }
        },
        cancellationToken);

    // Return an application identifier rather than exposing the storage account URL.
    return Ok(new { id = blobName });
}

The size limit in the example is illustrative: set limits that fit your application and configure the hosting layer accordingly. For larger uploads, check the request-size limits of your web server and hosting plan. If users regularly send very large files, consider having the application authorize an upload and then let the client upload directly to Blob Storage using a short-lived SAS URL. That avoids sending all the file bytes through your web server, but requires careful scoping and expiry for the SAS.

Names, validation, and downloads

Don’t use the original filename as the blob name. It may contain awkward characters, reveal personal information, or collide with another upload. Store a generated identifier as the blob name and keep the original name in your application’s database if users need to see it later. The database can also track the owner, upload time, scan status, and any business-specific permissions.

Neither the filename extension nor IFormFile.ContentType proves what a file contains; both come from the request. Validate the formats your application accepts, enforce size limits, and consider malware scanning before making uploaded files available. For downloads, have the application check the user’s authorization first. If you return a SAS URL for a private file, make it read-only, narrowly scoped, and short-lived.

Operational details worth deciding early

  • Failure handling: A storage call can fail even after request validation. Log useful diagnostic context, but avoid logging credentials or sensitive file contents.
  • Cleanup: Decide what happens to abandoned or expired files. Blob Storage lifecycle rules can move or delete blobs based on age, helping control storage costs.
  • Consistency: If the application stores file records in a database, plan what happens when the upload succeeds but the database write fails, or vice versa. A cleanup process for orphaned blobs is often simpler than trying to make two separate services participate in one transaction.
  • Access: Keep the container private and give the application only the permissions it needs. Test the managed identity permissions in the deployed environment, not just with a developer’s local credentials.

For a typical ASP.NET Core application, this pattern gives you streaming uploads, private storage, and an application-controlled way to identify and serve files. The best next step is to test it with the same identity and request-size limits the production app will use.

0 Comments Report