Skip to main content

Azure Blob Storage Provider

Vali-Blob.Azure provides AzureBlobProvider, implementing IStorageProvider, IResumableUploadProvider, and IPresignedUrlProvider backed by Azure Blob Storage via Azure.Storage.Blobs.


Installation

dotnet add package Vali-Blob.Core
dotnet add package Vali-Blob.Azure

AzureBlobOptions Reference

OptionTypeRequiredDescription
ConnectionStringstring?Yes*Storage account connection string. Not required when using Managed Identity.
AccountNamestring?Yes*Storage account name. Required when using Managed Identity (TokenCredential).
ContainerNamestringYesBlob container name for all operations.
CreateIfNotExistsboolNoCreate the container on startup if it does not exist. Default: false.
PublicAccessPublicAccessTypeNoContainer public access: None, Blob, or Container. Default: None.
DefaultBlobTierAccessTier?NoDefault access tier for uploaded blobs: Hot, Cool, or Archive. Default: null (inherits account default).

* Either ConnectionString or AccountName (with a registered TokenCredential) must be provided.


DI Registration

Connection String

using Vali-Blob.Core;
using Vali-Blob.Azure;

builder.Services
.AddVali-Blob(o => o.DefaultProvider = "azure")
.AddProvider<AzureBlobProvider>("azure", opts =>
{
opts.ConnectionString = builder.Configuration["Azure:ConnectionString"]!;
opts.ContainerName = builder.Configuration["Azure:ContainerName"]!;
opts.CreateIfNotExists = true;
})
.WithPipeline(p => p
.UseValidation(v =>
{
v.MaxFileSizeBytes = 500_000_000;
v.AllowedExtensions = [".jpg", ".png", ".pdf", ".mp4"];
})
.UseContentTypeDetection()
.UseConflictResolution(ConflictResolution.ReplaceExisting)
);

appsettings.json

{
"Azure": {
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=mystorageaccount;AccountKey=base64key==;EndpointSuffix=core.windows.net",
"ContainerName": "uploads"
}
}
Never commit credentials

Store connection strings in dotnet user-secrets for development. Use Azure Key Vault, environment variables, or managed secrets in production. Never commit connection strings to source control.


When running on Azure (App Service, AKS, Azure Functions, VMs), use Managed Identity instead of static credentials. This eliminates secret rotation concerns entirely.

Step 1: Enable Managed Identity

Enable a system-assigned Managed Identity on your compute resource in the Azure Portal, then assign it the Storage Blob Data Contributor RBAC role on your storage account.

Step 2: Register DefaultAzureCredential

using Azure.Identity;
using Azure.Core;

// Register the credential — automatically resolves Managed Identity in Azure,
// developer tooling (Azure CLI, VS, VS Code) locally
builder.Services.AddSingleton<TokenCredential>(new DefaultAzureCredential());

Step 3: Configure Vali-Blob with AccountName

builder.Services
.AddVali-Blob(o => o.DefaultProvider = "azure")
.AddProvider<AzureBlobProvider>("azure", opts =>
{
// No ConnectionString — TokenCredential resolved from DI
opts.AccountName = builder.Configuration["Azure:AccountName"]!;
opts.ContainerName = "uploads";
});

appsettings.json

{
"Azure": {
"AccountName": "mystorageaccount",
"ContainerName": "uploads"
}
}
DefaultAzureCredential credential chain

DefaultAzureCredential tries, in order: Environment variables → Workload Identity → Managed Identity → Azure Developer CLI → Azure CLI → Azure PowerShell → Azure CLI (interactive). In production Azure environments, Managed Identity is resolved automatically with no configuration.


Container Access Tiers

Access LevelDescription
NonePrivate — all access requires a valid SAS token or authenticated request. Recommended default.
BlobAnonymous read access for individual blobs. Container listing requires authentication.
ContainerFull public read access including listing. Use only for truly public assets.

Blob Access Tiers

TierAccess PatternNotes
HotFrequentDefault. Highest storage cost, lowest access latency and cost.
CoolInfrequent (30+ days)~50% lower storage cost. Early deletion fee if deleted before 30 days.
ArchiveRare (180+ days)Lowest cost. Requires rehydration (hours to days) before reading.

Configure DefaultBlobTier in AzureBlobOptions, or use lifecycle management rules in the Azure Portal to tier blobs automatically.


Presigned URLs (SAS Tokens)

AzureBlobProvider implements IPresignedUrlProvider. Presigned URLs use Shared Access Signatures (SAS) to grant time-limited, scoped access to a specific blob — your server never touches file data during client transfers:

var provider = factory.Create("azure");

if (provider is IPresignedUrlProvider presigned)
{
// SAS PUT URL — client uploads directly to Azure for 30 minutes
var uploadUrl = await presigned.GetPresignedUploadUrlAsync(
StoragePath.From("uploads", userId, "avatar.jpg"),
expiresIn: TimeSpan.FromMinutes(30));

// SAS GET URL — time-limited download for 2 hours
var downloadUrl = await presigned.GetPresignedDownloadUrlAsync(
"private/salary-report.pdf",
expiresIn: TimeSpan.FromHours(2));

return Results.Ok(new
{
uploadUrl = uploadUrl.Value,
downloadUrl = downloadUrl.Value
});
}

SAS tokens are self-contained: the expiry, permissions, and cryptographic signature are embedded in the URL. No server-side revocation or tracking is required; access expires automatically.


CORS Configuration for Direct Client Uploads

When clients upload directly via presigned SAS URLs, configure CORS on your storage account:

az storage cors add \
--services b \
--methods PUT GET HEAD \
--origins "https://myapp.com" \
--allowed-headers "*" \
--exposed-headers "ETag,x-ms-request-id" \
--max-age 3600 \
--account-name mystorageaccount

Azurite (Local Development)

Azurite is the official Azure Storage emulator for local development — no Azure account required:

# Via Docker
docker run -d --name azurite \
-p 10000:10000 \
mcr.microsoft.com/azure-storage/azurite \
azurite-blob --blobHost 0.0.0.0

Configure Vali-Blob to use Azurite:

.AddProvider<AzureBlobProvider>("azure", opts =>
{
opts.ConnectionString = "UseDevelopmentStorage=true";
opts.ContainerName = "dev-uploads";
opts.CreateIfNotExists = true;
})

The well-known UseDevelopmentStorage=true alias points to Azurite's default endpoint at http://127.0.0.1:10000/devstoreaccount1.


Multiple Containers

Configure multiple named providers to route different content types to different containers:

builder.Services
.AddVali-Blob(o => o.DefaultProvider = "azure-uploads")
.AddProvider<AzureBlobProvider>("azure-uploads", opts =>
{
opts.ConnectionString = config["Azure:ConnectionString"]!;
opts.ContainerName = "user-uploads";
})
.AddProvider<AzureBlobProvider>("azure-archives", opts =>
{
opts.ConnectionString = config["Azure:ConnectionString"]!;
opts.ContainerName = "archives";
opts.DefaultBlobTier = AccessTier.Cool;
});

// Route to specific container
var archiveProvider = factory.Create("azure-archives");

Lifecycle Management

Configure lifecycle rules in the Azure Portal to automatically tier or delete old blobs:

  1. Navigate to your storage account → Data managementLifecycle management
  2. Add rules:
RuleConditionAction
Tier to CoolLast modified > 30 daysMove to Cool tier
Tier to ArchiveLast modified > 90 daysMove to Archive tier
DeleteLast modified > 365 daysDelete blob

This is ideal for temporary uploads, expiring content, and compliance-driven retention policies.


Large File Uploads (Resumable)

Azure Blob Storage block blobs support resumable uploads via block staging. Vali-Blob maps its IResumableUploadProvider interface to the Azure Block Blob API:

Vali-Blob OperationAzure SDK Operation
StartResumableUploadAsyncInitializes a tracked session with a block list
UploadChunkAsyncStageBlockAsync — each chunk is a staged block
CompleteResumableUploadAsyncCommitBlockListAsync
AbortResumableUploadAsyncDiscards staged blocks

Maximum block size: 4,000 MB per block. Maximum blob size: 190.7 TiB.

ContentLength and Azure Blob

Azure Blob's StageBlockAsync requires a known content length for each block. Vali-Blob ensures chunks are fully buffered before staging, so the block size is always known. When the compression middleware is active, Vali-Blob uses chunked transfer encoding to avoid requiring the final compressed size upfront.


Supported Operations

OperationSupportedNotes
UploadAsyncYesBlock blob PUT or multipart
DownloadAsyncYesIncluding byte range (partial content)
DeleteAsyncYes
DeleteFolderAsyncYesBatch list + delete by prefix
ExistsAsyncYesBlobClient.ExistsAsync
CopyAsyncYesServer-side copy within the same account
GetMetadataAsyncYesBlobProperties + custom metadata
SetMetadataAsyncYesSetMetadataAsync
ListFilesAsyncYesGetBlobsAsync with prefix + pagination
ListFoldersAsyncYesGetBlobsByHierarchyAsync with delimiter
GetUrlAsyncYesPublic URL or SAS URL
StartResumableUploadAsyncYesBlock blob staging
UploadChunkAsyncYesStageBlockAsync
CompleteResumableUploadAsyncYesCommitBlockListAsync
AbortResumableUploadAsyncYesDiscard staged blocks
GetPresignedUploadUrlAsyncYesSAS PUT URL
GetPresignedDownloadUrlAsyncYesSAS GET URL