Azure Blob Storage Patterns for .NET Applications
Azure Blob Storage is deceptively simple on the surface — you put bytes in, you get bytes out. But in production .NET applications, the patterns you choose for uploading, downloading, and organising blobs make a real difference to performance, cost, and reliability.
This article covers the practical patterns you'll reach for most often with the Azure.Storage.Blobs SDK (v12+).
Client Setup
Register the BlobServiceClient in your DI container. When running in Azure with managed identity, this is all you need:
builder.Services.AddSingleton(new BlobServiceClient(
new Uri("https://myaccount.blob.core.windows.net"),
new DefaultAzureCredential()));
For local development, the connection string approach works with Azurite:
builder.Services.AddSingleton(new BlobServiceClient(
builder.Configuration.GetConnectionString("BlobStorage")));
Uploading Blobs
For small files, a straightforward upload is fine:
public async Task UploadDocumentAsync(string containerName, string blobName, Stream content)
{
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
await containerClient.CreateIfNotExistsAsync();
var blobClient = containerClient.GetBlobClient(blobName);
await blobClient.UploadAsync(content, new BlobUploadOptions
{
HttpHeaders = new BlobHttpHeaders
{
ContentType = "application/pdf"
},
Conditions = new BlobRequestConditions
{
IfNoneMatch = ETag.All // fail if blob already exists
}
});
}
The IfNoneMatch = ETag.All condition prevents accidental overwrites — a pattern worth adopting by default.
For large files, the SDK handles block uploads automatically when you set transfer options:
var options = new BlobUploadOptions
{
TransferOptions = new StorageTransferOptions
{
MaximumConcurrency = 4,
InitialTransferSize = 4 * 1024 * 1024, // 4 MB
MaximumTransferSize = 4 * 1024 * 1024 // 4 MB per block
}
};
await blobClient.UploadAsync(largeStream, options);
The SDK splits the stream into blocks and uploads them in parallel. You don't need to manage block IDs or commit block lists yourself.
Downloading Blobs
For downloading, stream the content rather than loading it all into memory:
public async Task<Stream> DownloadBlobAsync(string containerName, string blobName)
{
var blobClient = _blobServiceClient
.GetBlobContainerClient(containerName)
.GetBlobClient(blobName);
BlobDownloadStreamingResult result = await blobClient.DownloadStreamingAsync();
return result.Content;
}
If you need the blob's properties alongside the content:
var response = await blobClient.DownloadContentAsync();
var properties = response.Value.Details;
var content = response.Value.Content.ToObjectFromJson<MyDocument>();
Listing and Hierarchical Organisation
Blob storage is flat — there are no real folders. But the SDK supports a virtual hierarchy using delimiters:
public async IAsyncEnumerable<string> ListFoldersAsync(string containerName, string prefix)
{
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
await foreach (var item in containerClient.GetBlobsByHierarchyAsync(
delimiter: "/", prefix: prefix))
{
if (item.IsPrefix)
yield return item.Prefix;
}
}
For listing all blobs, use the flat listing with pagination handled automatically:
await foreach (var blob in containerClient.GetBlobsAsync(prefix: "invoices/2025/"))
{
Console.WriteLine($"{blob.Name} — {blob.Properties.ContentLength} bytes");
}
SAS Tokens for Temporary Access
When you need to give external clients temporary access to a blob without proxying the download through your API:
public Uri GenerateReadSasUri(string containerName, string blobName, TimeSpan expiry)
{
var blobClient = _blobServiceClient
.GetBlobContainerClient(containerName)
.GetBlobClient(blobName);
var sasBuilder = new BlobSasBuilder
{
BlobContainerName = containerName,
BlobName = blobName,
Resource = "b",
ExpiresOn = DateTimeOffset.UtcNow.Add(expiry)
};
sasBuilder.SetPermissions(BlobSasPermissions.Read);
return blobClient.GenerateSasUri(sasBuilder);
}
Keep SAS token lifetimes short — minutes, not hours. For user-facing downloads, generate the URI on demand and redirect the client to it.
Lifecycle Management in Code
You can set blob tiers programmatically to manage storage costs:
await blobClient.SetAccessTierAsync(AccessTier.Cool);
For archival blobs that are rarely accessed, move them to the Archive tier. Be aware that rehydration from Archive can take hours.
A Note on Azurite
For local development and integration testing, use Azurite — it's the official storage emulator and supports blobs, queues, and tables. Run it via Docker or npm, and point your connection string at UseDevelopmentStorage=true.
These patterns cover the vast majority of blob storage scenarios in .NET. The SDK is well-designed and handles the complexity of chunked uploads, retries, and streaming for you — lean into it.