Skip to content

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.