Blob Storage — Complete Reference¶
The hierarchy¶
Storage Account
└── Container (like a folder)
└── Blob (the actual file)
You never access a blob directly — you always enter through the Storage Account, navigate to a container, then to the blob. This mirrors the SDK:
BlobServiceClient → represents the Storage Account
└── BlobContainerClient → represents a Container
└── BlobClient → represents a single Blob
Storage Account¶
The top-level Azure resource. One storage account can hold multiple storage services:
Storage Account
├── Blob Storage → unstructured files, documents, images
├── Queue Storage → simple FIFO message queue
├── Table Storage → NoSQL key-value
└── File Storage → SMB network file shares
You pay per GB stored and per operation — not a flat rate. Creating a storage account costs nothing, you pay only for what you use.
Storage Account name is globally unique across all of Azure. Lowercase, no hyphens, 3-24 characters.
Performance tiers¶
Standard — magnetic/HDD backed. For most use cases — documents, images, backups, audit trails. Your use case.
Premium — SSD backed. For high-frequency low-latency scenarios — databases, high-throughput apps. More expensive. Not needed for PEPPOL or image processing.
Redundancy options¶
LRS (Locally Redundant) → 3 copies in one datacenter. Cheapest. Fine for learning/dev.
ZRS (Zone Redundant) → 3 copies across availability zones in one region.
GRS (Geo Redundant) → 6 copies across two regions. Good for audit trails in production.
GZRS → ZRS + GRS combined. Most resilient, most expensive.
For a PEPPOL audit trail in production — GRS is arguable. UBL documents as immutable compliance records warrant cross-region redundancy.
Blob types¶
Block blob — standard files. Documents, images, videos, anything you'd normally store. This is what you use always.
Append blob — optimised for append-only operations. Log files, audit streams. Each write appends to the end, you can't modify earlier blocks.
Page blob — optimised for random read/write access. Used internally by Azure for VM disk storage. You'll never use this directly.
Access tiers¶
Hot → frequently accessed data. Higher storage cost, lower operation cost.
Cool → infrequently accessed. Lower storage cost, higher operation cost. Min 30 day storage.
Cold → rarely accessed. Even lower storage cost. Min 90 day storage.
Archive → almost never accessed. Cheapest storage. Retrieval takes hours. Min 180 day storage.
For PEPPOL UBL documents — Hot initially, move to Cool after 90 days via lifecycle policy. Archive after a year if compliance allows.
For product images — Hot always, they're served to end users.
SDK — the three clients¶
// Entry point — always the Storage Account URL
var serviceClient = new BlobServiceClient(
new Uri("https://averblobstore.blob.core.windows.net"),
new DefaultAzureCredential()
);
// Navigate to container
var containerClient = serviceClient.GetBlobContainerClient("invoices");
// Navigate to specific blob
var blobClient = containerClient.GetBlobClient("invoice_001.xml");
You never jump straight to a container or blob — always enter through BlobServiceClient.
Core operations¶
// Upload — stream directly, no temp file needed
var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
await blobClient.UploadAsync(stream, overwrite: true);
// Download
var response = await blobClient.DownloadContentAsync();
var content = response.Value.Content.ToString();
// Delete
await blobClient.DeleteAsync();
// List blobs with optional prefix filter
await foreach (var blob in containerClient.GetBlobsAsync(prefix: "invoices/2026"))
{
Console.WriteLine(blob.Name);
}
UploadAsync vs UploadBlobAsync — UploadAsync on BlobClient supports overwrite: true. UploadBlobAsync on BlobContainerClient throws if blob already exists. Use UploadAsync on BlobClient always.
Virtual directories¶
Blob Storage is flat — no real folders. But forward slashes in blob names create virtual directories:
invoices/2026/april/INV-001.xml
invoices/2026/april/INV-002.xml
invoices/2026/march/INV-001.xml
Portal shows these as folders. Prefix filter works on them:
containerClient.GetBlobsAsync(prefix: "invoices/2026/april/")
Useful for multi-tenant data organisation:
{tenantId}/{invoiceNumber}.xml
Authentication¶
Three options, in order of preference:
Managed Identity (production on Azure)
new BlobServiceClient(
new Uri("https://averblobstore.blob.core.windows.net"),
new DefaultAzureCredential()
);
Requires Storage Blob Data Contributor role assigned to the identity on the storage account.
az login (local development) Same code as above — DefaultAzureCredential falls through to AzureCliCredential automatically. Requires Storage Blob Data Contributor role assigned to your user account.
Connection string (avoid in production)
new BlobServiceClient("DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;");
If you must use a connection string — store it in Key Vault, reference it via @Microsoft.KeyVault(...) in app settings. Never hardcode it.
RBAC roles for Blob Storage¶
Storage Blob Data Contributor → read/write/delete blobs. Your app needs this.
Storage Blob Data Reader → read only. Good for apps that only serve content.
Storage Blob Data Owner → full control including ACLs. Rarely needed.
Storage Account Contributor → manage the storage account itself, not blob data.
Ops uses this, not your app.
Role assignment is at the Storage Account level — applies to all containers. Or you can scope to a specific container if you need finer control.
Security defaults worth knowing¶
Blob anonymous access: Disabled by default
→ no public access without authentication
→ leave disabled unless you intentionally want public blobs
Storage account key access: Enabled by default
→ connection strings work
→ in production consider disabling to force identity-based auth only
Secure transfer required: Enabled by default
→ HTTPS only, HTTP rejected
→ never disable this
Server-side encryption: Always on, always free
→ all blobs encrypted at rest automatically
→ no configuration needed
The IDocumentStore abstraction¶
Raw BlobServiceClient should never leak into your domain or application layer. Wrap it:
// Interface — no Azure dependencies
public interface IDocumentStore
{
Task UploadAsync(string documentName, Stream content);
Task<Stream> DownloadAsync(string documentName);
Task DeleteAsync(string documentName);
}
// Implementation — Azure-specific, lives in infra layer
public class AzureBlobDocumentStore : IDocumentStore
{
private readonly BlobContainerClient _containerClient;
public AzureBlobDocumentStore(string accountUrl, string containerName)
{
var serviceClient = new BlobServiceClient(
new Uri(accountUrl),
new DefaultAzureCredential()
);
_containerClient = serviceClient.GetBlobContainerClient(containerName);
}
public async Task UploadAsync(string documentName, Stream content)
{
await _containerClient.GetBlobClient(documentName)
.UploadAsync(content, overwrite: true);
}
public async Task<Stream> DownloadAsync(string documentName)
{
var response = await _containerClient.GetBlobClient(documentName)
.DownloadContentAsync();
return response.Value.Content.ToStream();
}
public async Task DeleteAsync(string documentName)
{
await _containerClient.GetBlobClient(documentName).DeleteAsync();
}
}
Config-driven provider switching via factory:
public static class DocumentStoreFactory
{
public static IDocumentStore Create(DocumentStoreOptions options)
{
return options.Provider switch
{
"Azure" => new AzureBlobDocumentStore(options.AccountUrl, options.ContainerName),
"S3" => new S3DocumentStore(options.S3BucketName, options.S3Region),
_ => throw new InvalidOperationException($"Unknown provider: {options.Provider}")
};
}
}
DI registration via factory extension:
builder.Services.AddSingleton<IDocumentStore>(sp =>
DocumentStoreFactory.Create(
sp.GetRequiredService<IConfiguration>()
.GetSection("DocumentStore")
.Get<DocumentStoreOptions>()
)
);
Don't use Blob Storage as a query engine¶
Blob listing is not a search mechanism. GetBlobsAsync with prefix is prefix-only — no contains, no regex, no metadata filtering. For anything beyond "list blobs starting with X":
Upload blob → store URL in PostgreSQL
Query → hit PostgreSQL with whatever filters you need
Retrieve → fetch specific blob by URL
Your database is the index. Blob Storage is the file cabinet.
Gotchas¶
BlobAlreadyExists on upload — UploadBlobAsync throws if blob exists. Use blobClient.UploadAsync(stream, overwrite: true) instead.
Stream position — if you read a stream and then try to upload it, position is at the end. Reset with stream.Position = 0 before the second read.
Content-Type defaults to application/octet-stream — set it explicitly for proper content serving:
var options = new BlobUploadOptions
{
HttpHeaders = new BlobHttpHeaders { ContentType = "application/xml" }
};
await blobClient.UploadAsync(stream, options);
Listing is eventually consistent — upload a blob and immediately list the container — it might not appear yet. Use your database as the source of truth, not blob listing.
Soft delete is available — enable it on the container for accidental deletion recovery. Deleted blobs recoverable for up to 7 days.