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 UploadBlobAsyncUploadAsync 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 uploadUploadBlobAsync 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.