DKNet

DKNet.Svc.BlobStorage.AzureStorage

Azure Blob Storage implementation of IBlobService, backed by Azure.Storage.Blobs.

For the operations, models, and validation rules every provider shares, read the Abstractions page first — this page covers only what is specific to Azure.

✨ Why use it?

🚀 Quick Start

dotnet add package DKNet.Svc.BlobStorage.AzureStorage
// appsettings.json
// { "BlobService": { "AzureStorage": { "ConnectionString": "...", "ContainerName": "documents" } } }

builder.Services.AddAzureStorageAdapter(builder.Configuration);
public sealed class ReportStorage(IBlobService blobService)
{
    public Task<string> SaveReportAsync(Stream pdf, CancellationToken ct) =>
        blobService.SaveAsync(new BlobDetails.BlobData("reports/monthly.pdf", BinaryData.FromStream(pdf)), ct);
}

AddAzureStorageAdapter(IConfiguration) binds AzureStorageOptions from the "BlobService:AzureStorage" section and registers IBlobService → AzureStorageBlobService as Scoped; the call is idempotent. Set code-only options such as BlobServiceClientFactory with a follow-up Configure call — see the next section, and read the Gotchas before reaching for the Action<AzureStorageOptions> overload.

🧩 Features

Managed identity via BlobServiceClientFactory

BlobServiceClientFactory is the hook for Azure AD / managed-identity auth — there is no separate “use Azure AD” flag. When it is set it takes priority over ConnectionString; when neither is set, the first blob operation throws ArgumentException. Because the factory is a delegate, configuration cannot bind it — layer it on with the standard options API:

using Azure.Identity;
using Azure.Storage.Blobs;
using DKNet.Svc.BlobStorage.AzureStorage;

builder.Services.AddAzureStorageAdapter(builder.Configuration);
builder.Services.Configure<AzureStorageOptions>(options =>
{
    options.ContainerName = "documents";
    options.BlobServiceClientFactory = _ => Task.FromResult(
        new BlobServiceClient(
            new Uri("https://myaccount.blob.core.windows.net"),
            new DefaultAzureCredential()));
});

The factory receives the resolved AzureStorageOptions, so it can read your own configuration values off it, and it is invoked once per scoped service instance — the resulting container client is cached.

Container auto-creation

The first operation resolves the container client for ContainerName and calls CreateIfNotExistsAsync. The identity in use therefore needs container-create rights unless the container already exists.

Save and overwrite semantics

SaveAsync validates against BlobServiceOptions and then passes Overwrite straight through to the SDK’s UploadAsync. Unlike S3 and Local, this provider raises no InvalidOperationException of its own — a duplicate upload with Overwrite = false surfaces the SDK’s RequestFailedException (409 BlobAlreadyExists) instead:

var blob = new BlobDetails.BlobData("reports/2026/q1.pdf", BinaryData.FromString("..."))
{
    Overwrite = true
};
var location = await blobService.SaveAsync(blob, ct); // "reports/2026/q1.pdf"

SAS-based public URLs

GetPublicAccessUrl builds a read-only BlobSasBuilder for the single blob, starting now and expiring after expiresFromNow — default TimeSpan.FromDays(1). If the underlying client cannot sign a SAS (CanGenerateSasUri == false, i.e. it was built from a token credential rather than an account key), it throws NotSupportedException:

var url = await blobService.GetPublicAccessUrl(
    new BlobRequest("reports/monthly.pdf"),
    TimeSpan.FromMinutes(15),
    ct);

Recursive folder delete

A BlobRequest whose name has no file extension is a directory request. DeleteAsync then walks the prefix breadth-first, deleting nested blobs as it finds them and queuing nested prefixes, and finally removes the folder markers from the deepest level up. Directory detection is by convention — an entry with no content type and no content length is treated as a folder marker.

Listing a prefix

ListItemsAsync streams GetBlobsAsync for the prefix (leading slash removed) and populates Details from each blob’s properties; folder markers come back with Details as null.

⚙️ Configuration reference

AzureStorageOptions extends BlobServiceOptions:

Option Type Default Effect
ContainerName string (required) Target container; created on first use when missing.
ConnectionString string? null Storage account connection string. Ignored when BlobServiceClientFactory is set.
BlobServiceClientFactory Func<AzureStorageOptions, Task<BlobServiceClient>>? null Builds the BlobServiceClient yourself — the hook for managed identity or any custom client. Cannot be set from configuration binding.
Name (static) string "BlobService:AzureStorage" Configuration section key AddAzureStorageAdapter(IConfiguration) binds from.

The shared IncludedExtensions, MaxFileNameLength, and MaxFileSizeInMb checks apply unchanged.

🧱 Where it fits

Which of the two option properties you set decides how the BlobServiceClient is built, and that in turn decides whether GetPublicAccessUrl can sign anything:

Workflow diagram: AddAzureStorageAdapter supplies the options, the first operation picks BlobServiceClientFactory when set and otherwise ConnectionString, throws ArgumentException when neither is set, creates the container when missing, then runs uploads, listings and deletes, with SAS URL signing as a separate branch.

⚠️ Gotchas & limits