Skip to main content

Azure Blob Storage C# Tutorial: Upload & Download Files

Learn Azure Blob Storage in C# with .NET 8: upload, download, list, and delete files using the Azure.Storage.Blobs SDK. Runnable code examples inside.

If you are building a .NET application that needs to store images, documents, backups, logs, or any unstructured data in the cloud, Azure Blob Storage with C# is the go-to solution. It is cheap, virtually unlimited in scale, and the official Azure.Storage.Blobs SDK makes it straightforward to upload, download, list, and delete files from any .NET 8 app. In this tutorial you will learn how Azure Blob Storage works, how to connect to it securely from C#, and how to perform every common file operation with runnable code — plus the best practices and pitfalls that trip up most developers in production.

What Is Azure Blob Storage and Why Use It From C#?

Azure Blob Storage is Microsoft's object storage service for unstructured data. "Blob" stands for Binary Large Object — a fancy name for "any file". Instead of saving user uploads to a local disk (which does not scale across servers and disappears when a container restarts), you store them in Azure and reference them by URL.

The storage hierarchy has three levels:

  • Storage Account — the top-level namespace, e.g. mycompanystorage.
  • Container — like a top-level folder, e.g. invoices or profile-pictures.
  • Blob — the actual file, e.g. 2026/08/invoice-1042.pdf. Blob names can contain slashes, which gives you virtual folders.

Why choose it over a file system or a database column? Three reasons: it scales to petabytes without you managing servers, it is far cheaper than storing binary data in SQL Server, and it integrates natively with Azure CDN, Azure Functions, and Managed Identity for secure, passwordless access.

Setting Up the Azure Storage SDK for .NET

Install the modern SDK package (not the legacy WindowsAzure.Storage, which is deprecated):

dotnet add package Azure.Storage.Blobs
dotnet add package Azure.Identity

The SDK exposes three client classes that map directly to the hierarchy above:

  • BlobServiceClient — operations on the storage account.
  • BlobContainerClient — operations on one container.
  • BlobClient — operations on one blob (upload, download, delete).

Connecting With a Connection String (Development Only)

For local development, the quickest way to connect is a connection string from the Azure Portal (Storage Account → Access keys). Store it in appsettings.Development.json or user secrets — never in source control.

using Azure.Storage.Blobs;

string connectionString = builder.Configuration["AzureStorage:ConnectionString"]!;
var blobServiceClient = new BlobServiceClient(connectionString);

Connecting With Managed Identity (Production)

In production you should avoid account keys entirely. DefaultAzureCredential uses your Azure CLI login locally and a Managed Identity when deployed to App Service, Container Apps, or AKS. Grant the identity the Storage Blob Data Contributor role on the storage account.

using Azure.Identity;
using Azure.Storage.Blobs;

var blobServiceClient = new BlobServiceClient(
    new Uri("https://mycompanystorage.blob.core.windows.net"),
    new DefaultAzureCredential());

Why this matters: a leaked connection string grants full read/write access to every container in the account. Managed Identity has no secret to leak and can be scoped with RBAC.

Registering the Client With Dependency Injection

BlobServiceClient is thread-safe and designed to be reused, so register it as a singleton in ASP.NET Core:

// Program.cs
builder.Services.AddSingleton(sp =>
    new BlobServiceClient(
        new Uri(builder.Configuration["AzureStorage:Uri"]!),
        new DefaultAzureCredential()));

How to Upload a File to Azure Blob Storage in C#

Uploading is the most searched-for operation, so let's cover it thoroughly. First, create a reusable service class:

using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;

public class BlobStorageService
{
    private readonly BlobContainerClient _container;

    public BlobStorageService(BlobServiceClient serviceClient)
    {
        _container = serviceClient.GetBlobContainerClient("documents");
    }

    public async Task EnsureContainerAsync(CancellationToken ct = default)
    {
        // Idempotent: does nothing if the container already exists.
        await _container.CreateIfNotExistsAsync(PublicAccessType.None, cancellationToken: ct);
    }

    public async Task<Uri> UploadAsync(
        string blobName,
        Stream content,
        string contentType,
        CancellationToken ct = default)
    {
        BlobClient blob = _container.GetBlobClient(blobName);

        var options = new BlobUploadOptions
        {
            HttpHeaders = new BlobHttpHeaders { ContentType = contentType },
            Metadata = new Dictionary<string, string>
            {
                ["uploadedBy"] = "csharp-coder-app",
                ["uploadedAtUtc"] = DateTime.UtcNow.ToString("O")
            }
        };

        // Overwrites by default when using BlobUploadOptions.
        await blob.UploadAsync(content, options, ct);
        return blob.Uri;
    }
}

Uploading From an ASP.NET Core Controller

Here is how to wire it to an IFormFile upload endpoint. Note that we stream the file directly rather than copying it into a byte array — this keeps memory usage flat even for large uploads.

[ApiController]
[Route("api/files")]
public class FilesController : ControllerBase
{
    private readonly BlobStorageService _storage;

    public FilesController(BlobStorageService storage) => _storage = storage;

    [HttpPost]
    [RequestSizeLimit(50_000_000)] // 50 MB
    public async Task<IActionResult> Upload(IFormFile file, CancellationToken ct)
    {
        if (file is null || file.Length == 0)
            return BadRequest("No file provided.");

        // Never trust the client's filename for the blob path.
        string extension = Path.GetExtension(file.FileName);
        string blobName = $"{DateTime.UtcNow:yyyy/MM}/{Guid.NewGuid()}{extension}";

        await using Stream stream = file.OpenReadStream();
        Uri uri = await _storage.UploadAsync(blobName, stream, file.ContentType, ct);

        return Ok(new { blobName, url = uri.ToString() });
    }
}

Uploading a Local File or a String

The SDK also has convenient overloads for paths and in-memory data:

// From a file on disk
await blobClient.UploadAsync("C:\\reports\\summary.csv", overwrite: true);

// From a string (e.g. JSON)
string json = JsonSerializer.Serialize(order);
await blobClient.UploadAsync(BinaryData.FromString(json), overwrite: true);

Pitfall: the plain UploadAsync(stream) overload without overwrite: true throws a RequestFailedException (409 BlobAlreadyExists) if the blob exists. Decide explicitly whether overwriting is what you want.

How to Download a Blob in C#

There are two download patterns and choosing the wrong one is the most common performance mistake.

Download to a Stream (Recommended for Web APIs)

Streaming the blob straight to the HTTP response means your server never holds the whole file in memory:

public async Task<(Stream Content, string ContentType)?> DownloadAsync(
    string blobName, CancellationToken ct = default)
{
    BlobClient blob = _container.GetBlobClient(blobName);

    if (!await blob.ExistsAsync(ct))
        return null;

    BlobDownloadStreamingResult result = await blob.DownloadStreamingAsync(cancellationToken: ct);
    return (result.Content, result.Details.ContentType);
}

// Controller
[HttpGet("{*blobName}")]
public async Task<IActionResult> Download(string blobName, CancellationToken ct)
{
    var download = await _storage.DownloadAsync(blobName, ct);
    if (download is null) return NotFound();

    return File(download.Value.Content, download.Value.ContentType,
                Path.GetFileName(blobName));
}

Download to a Local File or Memory

// Save directly to disk
await blobClient.DownloadToAsync("C:\\downloads\\summary.csv");

// Load into memory (only for small blobs!)
BlobDownloadResult result = await blobClient.DownloadContentAsync();
string text = result.Content.ToString();
byte[] bytes = result.Content.ToArray();

Why not always use DownloadContentAsync? It buffers the entire blob in memory. A few concurrent requests for 200 MB video files will exhaust your app's RAM. Use streaming unless you know the blob is small.

Managing Files: List, Delete, Copy, and Metadata

Listing Blobs With a Prefix

Because blob names can contain slashes, listing by prefix gives you "folder" semantics. The SDK returns an AsyncPageable so you can iterate millions of blobs without loading them all at once:

public async Task<List<BlobItem>> ListAsync(string prefix, CancellationToken ct = default)
{
    var items = new List<BlobItem>();

    await foreach (BlobItem item in _container.GetBlobsAsync(
        traits: BlobTraits.Metadata, prefix: prefix, cancellationToken: ct))
    {
        items.Add(item);
        Console.WriteLine($"{item.Name} - {item.Properties.ContentLength} bytes, " +
                          $"modified {item.Properties.LastModified}");
    }

    return items;
}

// Usage: everything uploaded in August 2026
var augustFiles = await storage.ListAsync("2026/08/");

Deleting a Blob

public async Task<bool> DeleteAsync(string blobName, CancellationToken ct = default)
{
    BlobClient blob = _container.GetBlobClient(blobName);

    // Returns false instead of throwing if the blob does not exist.
    Response<bool> response = await blob.DeleteIfExistsAsync(
        DeleteSnapshotsOption.IncludeSnapshots, cancellationToken: ct);

    return response.Value;
}

Enable soft delete on the storage account (Data protection settings) so accidental deletes can be recovered for a retention period. It costs almost nothing and has saved many teams.

Copying Blobs Between Containers

Copies happen server-side — the bytes never travel through your app:

BlobClient source = serviceClient.GetBlobContainerClient("uploads").GetBlobClient("draft.pdf");
BlobClient dest   = serviceClient.GetBlobContainerClient("archive").GetBlobClient("2026/draft.pdf");

CopyFromUriOperation operation = await dest.StartCopyFromUriAsync(source.Uri);
await operation.WaitForCompletionAsync();

Reading and Updating Metadata and Properties

BlobProperties props = await blobClient.GetPropertiesAsync();
Console.WriteLine($"Size: {props.ContentLength}, Type: {props.ContentType}");
Console.WriteLine($"Uploaded by: {props.Metadata["uploadedBy"]}");

// Update metadata without re-uploading the content
await blobClient.SetMetadataAsync(new Dictionary<string, string>
{
    ["status"] = "approved",
    ["reviewedAtUtc"] = DateTime.UtcNow.ToString("O")
});

Generating SAS URLs for Secure, Time-Limited Access

Don't make containers public just so a browser can display an image. Instead, generate a Shared Access Signature (SAS) — a URL that grants read access for a limited time. With Managed Identity you use a user delegation key rather than the account key:

using Azure.Storage.Sas;

public async Task<Uri> GetReadUrlAsync(string blobName, TimeSpan validFor)
{
    BlobClient blob = _container.GetBlobClient(blobName);
    DateTimeOffset expires = DateTimeOffset.UtcNow.Add(validFor);

    UserDelegationKey key = await _serviceClient.GetUserDelegationKeyAsync(
        DateTimeOffset.UtcNow.AddMinutes(-5), expires);

    var sasBuilder = new BlobSasBuilder
    {
        BlobContainerName = _container.Name,
        BlobName = blobName,
        Resource = "b",
        ExpiresOn = expires
    };
    sasBuilder.SetPermissions(BlobSasPermissions.Read);

    var uriBuilder = new BlobUriBuilder(blob.Uri)
    {
        Sas = sasBuilder.ToSasQueryParameters(key, _serviceClient.AccountName)
    };

    return uriBuilder.ToUri();
}

// Usage: link valid for 15 minutes
Uri link = await storage.GetReadUrlAsync("2026/08/invoice-1042.pdf", TimeSpan.FromMinutes(15));

Azure Blob Storage C# Best Practices

  • Reuse clients. BlobServiceClient and BlobContainerClient are thread-safe and hold an HTTP connection pool. Creating one per request causes socket exhaustion.
  • Always pass a CancellationToken. When a user abandons a 100 MB download, you want the transfer to stop immediately.
  • Set ContentType on upload. Without it every blob is served as application/octet-stream, so browsers will download images instead of displaying them.
  • Use the right access tier. Hot for frequently accessed files, Cool for backups, Archive for compliance data you almost never read. You can set it per blob with blobClient.SetAccessTierAsync(AccessTier.Cool).
  • Handle RequestFailedException. Check ex.Status (404, 409, 412) and ex.ErrorCode (e.g. BlobErrorCode.BlobNotFound) rather than swallowing all errors.
  • Tune large transfers. For multi-gigabyte files, set StorageTransferOptions with MaximumConcurrency and InitialTransferSize on BlobUploadOptions to parallelize the block upload.
  • Test locally with Azurite. The UseDevelopmentStorage=true connection string points at the free Azurite emulator, so unit and integration tests never touch a real account.

Common Pitfalls to Avoid

  • Committing connection strings. Use user secrets locally and Key Vault or Managed Identity in production.
  • Using the client's original file name as the blob name. It invites collisions and path-traversal-style names like ../../etc. Generate your own names.
  • Loading whole files into byte[]. Stream everything; your memory graph will thank you.
  • Forgetting soft delete and versioning. A single buggy loop calling DeleteAsync can wipe a container in seconds.
  • Mixing up the legacy and new SDKs. Samples referencing CloudBlockBlob or CloudStorageAccount are for the deprecated WindowsAzure.Storage package. Stick with Azure.Storage.Blobs.

Conclusion: Key Takeaways

Working with Azure Blob Storage in C# comes down to three clients — BlobServiceClient, BlobContainerClient, and BlobClient — and a handful of async methods. Here is what to remember:

  • Install Azure.Storage.Blobs and register a singleton BlobServiceClient with DI.
  • Authenticate with DefaultAzureCredential and Managed Identity in production; connection strings only for local development.
  • Upload with UploadAsync plus BlobUploadOptions to set content type and metadata; stream from IFormFile rather than buffering.
  • Download with DownloadStreamingAsync for web responses and DownloadToAsync for disk; reserve DownloadContentAsync for small blobs.
  • List with prefixes, delete with DeleteIfExistsAsync, copy server-side, and share files with short-lived SAS URLs instead of public containers.

With these patterns in place you have a production-ready foundation for file storage in any .NET application. Next, explore Azure Blob Storage lifecycle management policies to automatically move old files to cheaper tiers, and consider pairing blobs with Azure CDN for fast global delivery of static assets.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...