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.
BlobServiceClient you built yourself with DefaultAzureCredential — no credential plumbing inside your own code.CreateIfNotExistsAsync runs on first use, so a fresh environment needs no
manual container step.GetPublicAccessUrl returns a SAS URI scoped to a single blob, valid for a
day unless you ask for less.DeleteAsync on a directory request walks
the prefix tree and removes the nested blobs and folder markers for you.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.
BlobServiceClientFactoryBlobServiceClientFactory 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.
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.
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"
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);
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.
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.
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.
Which of the two option properties you set decides how the BlobServiceClient is built, and that in
turn decides whether GetPublicAccessUrl can sign anything:
AzureStorageBlobService derives
from its BlobService base class; application code depends on IBlobService, and only the composition root
references this package.DKNet.EfCore.Events / DKNet.EfCore.Specifications — pair with them exactly as any provider does; see the
Abstractions page.GetAsync throws on a missing blob; it does not return null. Properties are fetched before the existence check,
so the SDK’s RequestFailedException (404) surfaces first and the method’s null path is unreachable. Treat a
404 as “not found” here, the same way you treat FileNotFoundException on the
Local provider.Action<AzureStorageOptions> overload does not configure the service. It registers the instance you populate
as a singleton AzureStorageOptions, while AzureStorageBlobService consumes IOptions<AzureStorageOptions> — the
values never reach it and the first operation throws ArgumentException. Use the IConfiguration overload, plus
services.Configure<AzureStorageOptions>(...) for anything configuration cannot bind.NotSupportedException from GetPublicAccessUrl is usually an auth-shape problem, not a bug. A client built from
a token credential (managed identity) cannot sign a SAS; an account-key client can. Check how the client was
constructed first.ListItemsAsync sets each result’s Name to the request’s name rather
than the blob’s own name — use Details for per-blob metadata and don’t rely on the listed name to address a blob.BlobServiceOptions checks.IBlobService providers can coexist in one IServiceCollection;
the last registration wins when a single IBlobService is resolved.