
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.
invoicesorprofile-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.
BlobServiceClientandBlobContainerClientare 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
ContentTypeon upload. Without it every blob is served asapplication/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. Checkex.Status(404, 409, 412) andex.ErrorCode(e.g.BlobErrorCode.BlobNotFound) rather than swallowing all errors. - Tune large transfers. For multi-gigabyte files, set
StorageTransferOptionswithMaximumConcurrencyandInitialTransferSizeonBlobUploadOptionsto parallelize the block upload. - Test locally with Azurite. The
UseDevelopmentStorage=trueconnection 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
DeleteAsynccan wipe a container in seconds. - Mixing up the legacy and new SDKs. Samples referencing
CloudBlockBloborCloudStorageAccountare for the deprecatedWindowsAzure.Storagepackage. Stick withAzure.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.Blobsand register a singletonBlobServiceClientwith DI. - Authenticate with
DefaultAzureCredentialand Managed Identity in production; connection strings only for local development. - Upload with
UploadAsyncplusBlobUploadOptionsto set content type and metadata; stream fromIFormFilerather than buffering. - Download with
DownloadStreamingAsyncfor web responses andDownloadToAsyncfor disk; reserveDownloadContentAsyncfor 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.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment