All articles

Azure for .NET Developers — Part 5: Blob Storage, Queues, and SAS Tokens That Expire Properly

BlobServiceClient with DefaultAzureCredential, access tiers and lifecycle rules, and user-delegation SAS links that expire in minutes instead of account keys that never do.

0 · log in to like, save & follow Share on LinkedIn Share on X
Azure for .NET Developers — Part 5: Blob Storage, Queues, and SAS Tokens That Expire Properly

Azure Storage is usually the second service a .NET team touches after a database, and it is deceptively deep: four sub-services, three access tiers, and at least three ways to authorize a request — two of which you should avoid. We will build up from the anatomy of a storage account to blob operations in C#, and finish with SAS tokens done properly, because expiry dates are where most storage security goes wrong.

Anatomy of a storage account

A storage account is a billing and management boundary that bundles four distinct services under one name and endpoint set:

Service What it stores Typical use .NET package
Blob Objects/files of any size Uploads, images, backups, static assets Azure.Storage.Blobs
Queue Small messages (64 KB) Simple background work handoff Azure.Storage.Queues
Table Schemaless key-value entities Cheap logs, lookup data Azure.Data.Tables
File SMB file shares Lift-and-shift apps expecting a drive Azure.Storage.Files.Shares

Blob storage carries most workloads and gets most of this post. Within an account, blobs live in containers — a flat namespace, though / in blob names gives you virtual folders that every tool renders as a tree.

Creating an account and container

az storage account create \
  --name stgeeksarray \
  --resource-group rg-geeksarray \
  --location westeurope \
  --sku Standard_LRS \
  --min-tls-version TLS1_2 \
  --allow-blob-public-access false

az storage container create \
  --name uploads \
  --account-name stgeeksarray \
  --auth-mode login

Two flags deserve attention. --allow-blob-public-access false prevents anyone from ever making a container anonymously readable — the cause of a steady stream of data-leak headlines. --auth-mode login makes the CLI use your Entra token instead of fishing for the account key, consistent with the managed identity approach from Part 4. To actually create containers and blobs with your own account, grant yourself Storage Blob Data Contributor on the account.

How do I upload a file to Azure Blob Storage from C#?

Install Azure.Storage.Blobs, create a BlobServiceClient with DefaultAzureCredential pointed at your account's blob endpoint, get a container client, and call UploadAsync on a blob client. No account key or connection string is needed if your identity has the Storage Blob Data Contributor role. The upload is a single call for small files and automatically chunked for large ones.

using Azure.Identity;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;

var service = new BlobServiceClient(
    new Uri("https://stgeeksarray.blob.core.windows.net"),
    new DefaultAzureCredential());

BlobContainerClient container = service.GetBlobContainerClient("uploads");

// Upload with content type so browsers render it correctly later
BlobClient blob = container.GetBlobClient("reports/2026/august.pdf");
await using (var stream = File.OpenRead("august.pdf"))
{
    await blob.UploadAsync(stream, new BlobUploadOptions
    {
        HttpHeaders = new BlobHttpHeaders { ContentType = "application/pdf" }
    });
}

// Download
Response<BlobDownloadResult> download = await blob.DownloadContentAsync();
byte[] bytes = download.Value.Content.ToArray();

// List everything under a virtual folder
await foreach (BlobItem item in container.GetBlobsAsync(prefix: "reports/2026/"))
{
    Console.WriteLine($"{item.Name}  {item.Properties.ContentLength} bytes");
}

Register BlobServiceClient as a singleton in DI — it is thread-safe and manages its own connection pooling. The Microsoft.Extensions.Azure package's AddAzureClients helper does this cleanly and shares one credential across all Azure SDK clients.

Access tiers and lifecycle rules

Every block blob sits in a tier that trades storage price against access price. Hot is the default: cheapest to read, most expensive to hold. Cool costs roughly half as much at rest but charges more per operation and expects data to stay put for 30 days. Archive is an order of magnitude cheaper again, but blobs are offline — reading one means a rehydration that takes up to several hours, which makes it strictly for compliance and backup data.

The practical move is not to set tiers manually but to let a lifecycle policy do it:

{
  "rules": [
    {
      "enabled": true,
      "name": "age-out-uploads",
      "type": "Lifecycle",
      "definition": {
        "filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["uploads/reports/"] },
        "actions": {
          "baseBlob": {
            "tierToCool": { "daysAfterModificationGreaterThan": 30 },
            "tierToArchive": { "daysAfterModificationGreaterThan": 180 },
            "delete": { "daysAfterModificationGreaterThan": 2555 }
          }
        }
      }
    }
  ]
}

Apply it with az storage account management-policy create --account-name stgeeksarray --resource-group rg-geeksarray --policy @policy.json. Blobs then age out of the expensive tier without any code on our side.

SAS tokens that actually expire

Sooner or later you need to give a browser or a third party direct access to one blob — a download link, or a direct-to-storage upload that bypasses your API. That is what a shared access signature (SAS) is for: a signed URL granting specific permissions on a specific resource until a specific time.

The classic way to sign one is with the account key, and that is exactly the smell to avoid: it drags the account key back into your configuration, and any SAS it signed remains valid until the key itself is rotated. The better tool is a user delegation SAS, signed with a key obtained via Entra — meaning managed identity works, no account key exists anywhere, and revoking the delegation key kills every SAS derived from it.

using Azure.Storage.Blobs;
using Azure.Storage.Sas;

// Requires the identity to hold "Storage Blob Delegator" (or Data Contributor) on the account
UserDelegationKey key = await service.GetUserDelegationKeyAsync(
    startsOn: DateTimeOffset.UtcNow.AddMinutes(-5),   // clock-skew buffer
    expiresOn: DateTimeOffset.UtcNow.AddHours(1));

var sasBuilder = new BlobSasBuilder
{
    BlobContainerName = "uploads",
    BlobName = "reports/2026/august.pdf",
    Resource = "b",
    ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(15)
};
sasBuilder.SetPermissions(BlobSasPermissions.Read);

BlobUriBuilder uriBuilder = new(blob.Uri)
{
    Sas = sasBuilder.ToSasQueryParameters(key, service.AccountName)
};

Uri downloadUrl = uriBuilder.ToUri();   // hand this to the browser

For browser-direct uploads the pattern is the mirror image: your API issues a SAS with Write and Create permissions on a specific blob name it chose, the browser PUTs the file straight to storage, and your server never proxies the bytes. That keeps large uploads off your web tier entirely while your API stays in control of naming and authorization.

Fifteen minutes is a sensible default for a download link the user clicks immediately. Resist the urge to issue year-long tokens "so support stops asking" — a SAS URL in an email thread or a browser history is a credential, and short expiry is the only mitigation you control after it leaves your server. User delegation keys themselves max out at seven days, which is Azure quietly enforcing the same opinion.

Queues: the honest paragraph

Storage queues are the simplest queueing primitive Azure has: put a message in, a worker pulls it out, deletes it when done, and messages that are pulled but never deleted become visible again for retry. For "resize this image later" or "send this email eventually" they are perfect and cost almost nothing. What they do not have: topics and subscriptions, ordered delivery, duplicate detection, transactions, or messages over 64 KB. If you catch yourself building any of those on top of storage queues, that is your cue to move to Service Bus — not before. We have seen teams pay for Service Bus namespaces to do work a storage queue handles with two lines of QueueClient code, and the reverse: fragile ordering hacks that Service Bus sessions would have solved outright.

Everything above is runnable from the 05-storage folder of the companion repo at github.com/laxmikant-geek/azure-for-dotnet-examples, including the lifecycle policy JSON.

What's next: queues naturally raise the question of what consumes them. In Part 6 we compare Azure Functions with the .NET background services you already know — cold starts with honest numbers, the isolated worker model, and when serverless genuinely wins versus when it is just extra moving parts.

Part 5 of 8 — don't miss the rest New parts of Azure for .NET Developers publish every few days. Get each one by email the day it lands — no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.