Skip to content
Back to skills

Dknet Blob Storage

ASecurity

Covers DKNet.Svc.BlobStorage.Abstractions, the .NET blob storage abstraction (IBlobService, BlobService, BlobRequest, BlobDetails.BlobData/BlobStreamData/BlobResult/BlobDataResult, BlobServiceOptions), plus its three providers: DKNet.Svc.BlobStorage.Local (AddLocalDirectoryBlobService, path-traversal-safe filesystem storage), DKNet.Svc.BlobStorage.AwsS3 (AddS3BlobService, AWS S3 and S3-compatible endpoints like MinIO and Cloudflare R2), and DKNet.Svc.BlobStorage.AzureStorage (AddAzureStorageA...

  • 6 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 22, 2026
devopsrustgoexpressdockerawsazuretestinggitapisecurity

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned September 22, 2026

npx -y skills add baoduy/DKNet --skill dknet-blob-storage --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dknet Blob Storage?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dknet Blob Storage
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/baoduy-dknet-blob-storage/badge)](https://www.skillsdirectory.com/skills/baoduy-dknet-blob-storage)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dknet-blob-storage
description: "Covers DKNet.Svc.BlobStorage.Abstractions, the .NET blob storage abstraction (IBlobService, BlobService, BlobRequest, BlobDetails.BlobData/BlobStreamData/BlobResult/BlobDataResult, BlobServiceOptions), plus its three providers: DKNet.Svc.BlobStorage.Local (AddLocalDirectoryBlobService, path-traversal-safe filesystem storage), DKNet.Svc.BlobStorage.AwsS3 (AddS3BlobService, AWS S3 and S3-compatible endpoints like MinIO and Cloudflare R2), and DKNet.Svc.BlobStorage.AzureStorage (AddAzureStorageAdapter, managed identity, SAS URLs). Use to upload, download, list, or delete files; wire DI/IConfiguration binding; generate a public or SAS URL; stream a large upload without buffering; test with Azurite or MinIO containers; or debug FileLoadException, InvalidOperationException, UnauthorizedAccessException path-traversal, or a GetAsync miss that throws on Local but returns null on S3/Azure."
license: MIT
metadata:
  author: baoduy
  version: "0.1.0"
  packages: "DKNet.Svc.BlobStorage.Abstractions,DKNet.Svc.BlobStorage.AwsS3,DKNet.Svc.BlobStorage.AzureStorage,DKNet.Svc.BlobStorage.Local"
---

# DKNet blob storage abstraction and adapters

This skill teaches the `IBlobService` contract and its three provider packages: how to register one, save/read/
list/delete blobs, generate public URLs, and avoid the traps that differ across Local, S3, and Azure. Open
`references/<PackageId>.md` for full member tables, runtime behaviour, and testing notes per package.

## Packages

| Package | Install | What it gives you | Depends on | Reference |
|---|---|---|---|---|
| `DKNet.Svc.BlobStorage.Abstractions` | `dotnet add package DKNet.Svc.BlobStorage.Abstractions` | `IBlobService`, `BlobService` base class, `BlobRequest`/`BlobDetails.*` records, `BlobServiceOptions` | none | [reference](references/DKNet.Svc.BlobStorage.Abstractions.md) |
| `DKNet.Svc.BlobStorage.Local` | `dotnet add package DKNet.Svc.BlobStorage.Local` | `AddLocalDirectoryBlobService` — filesystem-backed `IBlobService` with path-traversal protection | Abstractions | [reference](references/DKNet.Svc.BlobStorage.Local.md) |
| `DKNet.Svc.BlobStorage.AwsS3` | `dotnet add package DKNet.Svc.BlobStorage.AwsS3` | `AddS3BlobService` — `IBlobService` over AWS S3, MinIO, or Cloudflare R2 | Abstractions | [reference](references/DKNet.Svc.BlobStorage.AwsS3.md) |
| `DKNet.Svc.BlobStorage.AzureStorage` | `dotnet add package DKNet.Svc.BlobStorage.AzureStorage` | `AddAzureStorageAdapter` — `IBlobService` over Azure Blob Storage, managed identity + SAS URLs | Abstractions | [reference](references/DKNet.Svc.BlobStorage.AzureStorage.md) |

## Quick start

```csharp
using DKNet.Svc.BlobStorage.Abstractions;
using System.Threading;

var builder = WebApplication.CreateBuilder(args);

// appsettings.json: { "BlobStorage": { "LocalFolder": { "RootFolder": "/var/app/storage" } } }
builder.Services.AddLocalDirectoryBlobService(builder.Configuration);

var app = builder.Build();

app.MapPost("/files/{name}", async (string name, IBlobService blobService, HttpRequest request, CancellationToken ct) =>
{
    var data = await BinaryData.FromStreamAsync(request.Body, ct);
    var location = await blobService.SaveAsync(new BlobDetails.BlobData(name, data) { Overwrite = true }, ct);
    return Results.Ok(location);
});

app.Run();
```

Swap `AddLocalDirectoryBlobService` for `AddS3BlobService` or `AddAzureStorageAdapter` to target S3-compatible
storage or Azure Blob Storage — application code above the DI registration line never changes.

## Rules

1. Depend on `IBlobService` (from `DKNet.Svc.BlobStorage.Abstractions`) everywhere in application code. Never
   reference `LocalBlobService`, `S3BlobService`, or `AzureStorageBlobService` directly — each provider registers
   only the interface, so `GetRequiredService<S3BlobService>()`-style resolution throws.
2. Register exactly one provider's DI extension at the composition root: `AddLocalDirectoryBlobService`,
   `AddS3BlobService`, or `AddAzureStorageAdapter`. Registering more than one does not error — the last
   registration wins whenever `IBlobService` is resolved as a single instance.
3. `using DKNet.Svc.BlobStorage.AwsS3;` is required to call `AddS3BlobService` — its `S3Setup` class sits in a
   normal namespace, unlike `AddLocalDirectoryBlobService` and `AddAzureStorageAdapter`, which are declared ambient
   under `Microsoft.Extensions.DependencyInjection` and need no extra `using` to *call* (their option types still
   do: `LocalDirectoryOptions`, `AzureStorageOptions`).
4. Registration lifetimes differ: Local is **Scoped**; S3 and Azure are both **Singleton**. Don't assume a fresh
   provider instance per request for S3/Azure, and don't assume a shared one for Local.
5. `GetAsync`'s "missing blob" signal differs per provider: Local throws `FileNotFoundException`; S3 and Azure
   both return `null`. `CheckExistsAsync` returns `false` on every provider — prefer it when only presence
   matters.
6. `Overwrite` on `BlobData`/`BlobStreamData` defaults to `false`. Local and S3 throw `InvalidOperationException`
   on a duplicate save without it; Azure instead lets the SDK's `RequestFailedException` (409, "BlobAlreadyExists")
   surface unwrapped.
7. `MaxFileSizeInMb`, `MaxFileNameLength`, and `IncludedExtensions` all default to disabled (`0`/`0`/`[]`) — there
   is no built-in cap unless you set one, and the size math is decimal MB (`* 1_000_000`), not binary MiB.
8. A blob name with no file extension makes `BlobRequest.Type` default to `BlobTypes.Directory` — a bare name
   like `"reports"` needs no extra flag to be treated as a folder, but a genuinely extension-less file needs
   `Type = BlobTypes.File` set explicitly, or a delete against it becomes a recursive folder delete.
9. Never hand-roll a `..`-string path-traversal check around the Local provider — its `GetFinalPath` guard already
   resolves and normalizes every request and throws `UnauthorizedAccessException` outside `RootFolder`.
10. On Azure, set `BlobServiceClientFactory` via `services.Configure<AzureStorageOptions>(...)` in code, never
    through the `Action<AzureStorageOptions>` overload of `AddAzureStorageAdapter` — that overload's values never
    reach the resolved `IOptions<AzureStorageOptions>` (see Gotchas).

## How to ...

### Save a blob and allow overwrite

**When**: writing or replacing a file by name.

```csharp
using DKNet.Svc.BlobStorage.Abstractions;
using System.Threading;

var services = new ServiceCollection();
services.AddLocalDirectoryBlobService(new ConfigurationBuilder().Build());
var blobService = services.BuildServiceProvider().GetRequiredService<IBlobService>();

var draft = new BlobDetails.BlobData("reports/2026/q1.pdf", BinaryData.FromString("first draft"));
await blobService.SaveAsync(draft, CancellationToken.None);

var final = new BlobDetails.BlobData("reports/2026/q1.pdf", BinaryData.FromString("final")) { Overwrite = true };
await blobService.SaveAsync(final, CancellationToken.None);
```

Notes: a second save without `Overwrite = true` throws on Local/S3 (`InvalidOperationException`); Azure surfaces
the SDK's 409 instead.

### Read a blob back and handle a missing one

**When**: code must run against any of the three providers without crashing on a miss.

```csharp
using DKNet.Svc.BlobStorage.Abstractions;
using System.IO;
using System.Threading;

var services = new ServiceCollection();
services.AddLocalDirectoryBlobService(new ConfigurationBuilder().Build());
var blobService = services.BuildServiceProvider().GetRequiredService<IBlobService>();

async Task<BinaryData?> TryReadAsync(IBlobService service, string name, CancellationToken ct)
{
    try
    {
        var result = await service.GetAsync(new BlobRequest(name), ct);
        return result?.Data; // S3 and Azure both return null here instead of throwing
    }
    catch (FileNotFoundException)
    {
        return null; // Local-only signal for a missing blob
    }
}

var content = await TryReadAsync(blobService, "reports/2026/q1.pdf", CancellationToken.None);
```

Notes: `CheckExistsAsync` returns `false` on every provider for a miss — prefer it when you only need presence,
not content.

### List everything under a prefix

**When**: enumerating a "folder" recursively.

```csharp
using DKNet.Svc.BlobStorage.Abstractions;
using System.Threading;

var services = new ServiceCollection();
services.AddLocalDirectoryBlobService(new ConfigurationBuilder().Build());
var blobService = services.BuildServiceProvider().GetRequiredService<IBlobService>();

var fileNames = new List<string>();
await foreach (var item in blobService.ListItemsAsync(new BlobRequest("reports") { Type = BlobTypes.Directory }, CancellationToken.None))
    if (item.Type == BlobTypes.File)
        fileNames.Add(item.Name);
```

Notes: S3 pages internally via `ContinuationToken` until every object is returned; Local performs one interleaved
filesystem walk. Do not assume "files, then directories" ordering on any provider.

### Stream a large upload without buffering it into memory

**When**: the payload is big enough that materializing it as `BinaryData` first is wasteful.

```csharp
using DKNet.Svc.BlobStorage.Abstractions;
using System.IO;
using System.Threading;

var services = new ServiceCollection();
services.AddLocalDirectoryBlobService(new ConfigurationBuilder().Build());
var blobService = services.BuildServiceProvider().GetRequiredService<IBlobService>();

await using var fileStream = File.OpenRead("large-export.csv");
var savedName = await blobService.SaveAsync(
    new BlobDetails.BlobStreamData("exports/large-export.csv", fileStream) { Overwrite = true },
    CancellationToken.None);
```

Notes: all three shipped providers override `SaveAsync(BlobStreamData, ...)` to write straight from the stream;
only a hand-rolled `IBlobService`/`BlobService` subclass that skips that override falls back to the buffering
default.

### Get a time-limited SAS or pre-signed URL

**When**: sharing a private blob without handing out credentials. Not supported on Local.

```csharp
using DKNet.Svc.BlobStorage.Abstractions;
using DKNet.Svc.BlobStorage.AzureStorage;
using System.Threading;

var services = new ServiceCollection();
services.AddAzureStorageAdapter(new ConfigurationBuilder().Build());
services.Configure<AzureStorageOptions>(o =>
{
    o.ConnectionString = "UseDevelopmentStorage=true";
    o.ContainerName = "documents";
});
var blobService = services.BuildServiceProvider().GetRequiredService<IBlobService>();

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

Notes: S3's `GetPublicAccessUrl` returns a pre-signed URL the same way. Azure throws `NotSupportedException` for
a managed-identity (token-credential) client — SAS needs an account-key/connection-string client. Local always
throws `NotSupportedException`.

### Point the S3 provider at MinIO or Cloudflare R2

**When**: an S3-compatible endpoint instead of real AWS.

```csharp
using DKNet.Svc.BlobStorage.Abstractions;
using DKNet.Svc.BlobStorage.AwsS3;

var configuration = new ConfigurationBuilder()
    .AddInMemoryCollection(new Dictionary<string, string?>
    {
        ["BlobService:S3:ConnectionString"] = "http://localhost:9000",
        ["BlobService:S3:BucketName"] = "app-documents",
        ["BlobService:S3:AccessKey"] = "minioadmin",
        ["BlobService:S3:Secret"] = "minioadmin",
        ["BlobService:S3:ForcePathStyle"] = "true"
    })
    .Build();

var services = new ServiceCollection();
services.AddS3BlobService(configuration);
var blobService = services.BuildServiceProvider().GetRequiredService<IBlobService>();
```

Notes: `ForcePathStyle = true` is required for MinIO and most S3-compatible services. For Cloudflare R2 also set
`DisablePayloadSigning = true`. `RegionEndpointName` has no effect either way — encode a region, if any, into
`ConnectionString`.

### Use Azure managed identity instead of a connection string

**When**: no account key/connection string is allowed.

```csharp
using Azure.Core;
using Azure.Storage.Blobs;
using DKNet.Svc.BlobStorage.Abstractions;
using DKNet.Svc.BlobStorage.AzureStorage;

// credential: pass in `new Azure.Identity.DefaultAzureCredential()` from the composition root.
public sealed class ManagedIdentityBlobSetup(TokenCredential credential)
{
    public IBlobService Build()
    {
        var services = new ServiceCollection();
        services.AddAzureStorageAdapter(new ConfigurationBuilder().Build()); // binds ContainerName, etc.
        services.Configure<AzureStorageOptions>(o =>
        {
            o.ContainerName = "documents";
            o.BlobServiceClientFactory = _ => Task.FromResult(
                new BlobServiceClient(new Uri("https://myaccount.blob.core.windows.net"), credential));
        });
        return services.BuildServiceProvider().GetRequiredService<IBlobService>();
    }
}
```

Notes: use the `IConfiguration` overload of `AddAzureStorageAdapter` plus `Configure<AzureStorageOptions>`, not
the `Action<AzureStorageOptions>` overload — that overload's values never reach the resolved options. A
managed-identity client generally cannot sign SAS URLs. `DKNet.Svc.BlobStorage.AzureStorage`'s own `.csproj` does
not reference `Azure.Identity` — but don't add it yourself either: `Azure.Storage.Blobs` already pulls in an
`Azure.Core` version that re-exports the whole `Azure.Identity` credential surface (`DefaultAzureCredential`
included) under that same namespace, so `using Azure.Identity;` plus `new DefaultAzureCredential()` at the call
site (the `TokenCredential credential` passed into `ManagedIdentityBlobSetup` above) already resolves it, no
extra package needed. Explicitly adding the standalone `Azure.Identity` package on top gives you two same-named
`DefaultAzureCredential` types and fails the build with `error CS0433: The type 'DefaultAzureCredential' exists
in both 'Azure.Core...' and 'Azure.Identity...'`.

### Test code that depends on IBlobService without Docker

**When**: unit/integration-testing application code that only needs *some* `IBlobService`, not S3/Azure-specific
behavior.

```csharp
using DKNet.Svc.BlobStorage.Abstractions;
using System.IO;

public sealed class BlobFixture : IDisposable
{
    public BlobFixture()
    {
        TestRoot = Path.Combine(Path.GetTempPath(), "test-blobs-" + Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(TestRoot);

        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["BlobStorage:LocalFolder:RootFolder"] = TestRoot
            })
            .Build();

        Service = new ServiceCollection()
            .AddLogging()
            .AddLocalDirectoryBlobService(config)
            .BuildServiceProvider()
            .GetRequiredService<IBlobService>();
    }

    public IBlobService Service { get; }
    public string TestRoot { get; }

    public void Dispose()
    {
        if (Directory.Exists(TestRoot)) Directory.Delete(TestRoot, true);
    }
}
```

Notes: reach for Azurite/MinIO containers (see the provider reference pages' Testing notes) only when the test
needs to verify S3- or Azure-specific behavior; Local's filesystem backing is faster and needs no Docker for
everything else.

## Runtime behaviour

Every save funnels through the same shape regardless of provider: `ValidateFile` (name/extension/size, shared
from Abstractions) → `GetBlobLocation` (path/key normalization) → the provider's own SDK write. The native client
each cloud provider needs (`AmazonS3Client`, `BlobContainerClient`) is built lazily on the *first* call after
registration and cached for the life of the resolved `IBlobService` instance — so the first call after startup
pays a real network/bucket-or-container-ensure round trip on S3 and Azure; Local pays nothing extra since it has
no client to build.

## Gotchas

- **`S3Options.RegionEndpointName` is inert** — building the S3 client never reads it. Encode the region into
  `ConnectionString` instead.
- **S3 bucket auto-creation on first use.** A `BucketName` typo silently creates a new empty bucket rather than
  failing — double-check `BucketName` before deploying.
- **S3 folder delete is one `DeleteObjectAsync` per key, not a batch call** (this SDK version's batch API needs a
  Content-MD5 header that MinIO rejects). Expect O(n) round trips deleting a very large prefix against real AWS
  S3.
- **Local's `RootFolder` defaults to `{CurrentDirectory}/LocalStore` when unset** — that directory differs
  between `dotnet run`, a published binary, and a container. Always set `RootFolder` explicitly outside a quick
  local run.
- **`ListItemsAsync`'s `Details.ContentType` on S3 is always `""`.** Call `GetAsync` for the real content type;
  don't trust the listing for it.
- **`SizeLimitedStream` (the non-seekable-stream size guard used internally) is read-only and forward-only** —
  `Seek`/`SetLength`/`Write`/`Length`/`Position` all throw `NotSupportedException`. Only sequential
  `Read`/`ReadAsync` works on a stream handed back from `ValidateFile(BlobStreamData)`.
- **`IBlobService`'s own default interface members for `OpenReadAsync`/`SaveAsync(BlobStreamData, ...)` still
  fully buffer the payload.** Only a provider that overrides them (all three shipped providers do) avoids that —
  a hand-rolled provider that skips the override gets no memory benefit from calling them.

## Do not

- `IBlobService.SaveAsync(byte[] data, ...)` — no such overload exists. Wrap arrays with `BinaryData.FromBytes(...)`.
- `IBlobServiceFactory`, or any provider registry/selector type — does not exist. DI registration order ("the
  last registration wins") is the only selection mechanism.
- `S3Options.Region` / `S3Options.RegionEndpoint` — the real (inert) property is `RegionEndpointName`.
- `AddLocalBlobService`, `AddLocalBlobStorage`, `UseLocalBlobStorage` — the real method is
  `AddLocalDirectoryBlobService`.
- `LocalBlobServiceOptions` / `LocalStorageOptions` — the real type is `LocalDirectoryOptions`.
- `AzureStorageOptions.UseManagedIdentity` — no such flag. Managed identity is expressed only by setting
  `BlobServiceClientFactory` and leaving `ConnectionString` unset.
- `GetRequiredService<S3BlobService>()` / `<AzureStorageBlobService>()` / `<LocalBlobService>()` — only
  `IBlobService` is registered; resolving a concrete provider type throws `InvalidOperationException` unless you
  registered it yourself.
- `new AmazonS3Client(...)` handed into `S3BlobService` — there is no constructor overload that accepts a
  pre-built client.
- `S3Options.MaxKeys` or any page-size option — pagination is automatic and internal, not caller-tunable.

## Related skills

- `dknet-packages` — start there to confirm blob storage is the right package family before wiring DI.
- `dknet-efcore-domain-model` — for the aggregate/entity that stores a blob's returned location string as a plain
  property.
- `dknet-efcore-specifications` — for querying/persisting entities that reference a blob location; `IBlobService`
  itself never appears in a specification.
- `dknet-efcore-save-pipeline` — to raise a domain event after `SaveAsync` returns a location, rather than
  calling `IBlobService` from inside a `SaveChanges` interceptor.
- `dknet-efcore-data-security` — for encrypting/row-securing the entity that references a blob, not the blob
  bytes themselves (that's `dknet-services`).
- `dknet-codegen` — unrelated to blob storage; route here only for `[CrudAction]`/`[GenerateDto]` questions.
- `dknet-slimbus-cqrs` — for the command/handler that calls `IBlobService` as part of a CQRS vertical slice.
- `dknet-aspcore-api` — for minimal-API endpoint conventions around the upload/download endpoint itself, not the
  storage call.
- `dknet-idempotency` — for making a repeated upload request safe to retry; blob storage's own `Overwrite` flag
  is not idempotency.
- `dknet-services` — for encrypting blob bytes before `SaveAsync`, or generating a PDF to then store via
  `IBlobService`.
- `dknet-core-utilities` — for a random file-name/key generator to hand to `SaveAsync`; not blob-storage-specific
  itself.
- `dknet-testing` — for TestContainers fixture conventions (Azurite/MinIO) beyond what this skill's reference
  pages already cover, and for testing inside the DKNet repo itself.

## References

- [references/DKNet.Svc.BlobStorage.Abstractions.md](references/DKNet.Svc.BlobStorage.Abstractions.md) — the
  `IBlobService` contract, `BlobService` base class, request/result records, shared validation options, and how
  to write your own provider.
- [references/DKNet.Svc.BlobStorage.AwsS3.md](references/DKNet.Svc.BlobStorage.AwsS3.md) — `S3BlobService`/
  `S3Options`, MinIO/R2 endpoint configuration, pagination, and folder-delete mechanics.
- [references/DKNet.Svc.BlobStorage.AzureStorage.md](references/DKNet.Svc.BlobStorage.AzureStorage.md) —
  `AzureStorageBlobService`/`AzureStorageOptions`, managed identity, SAS URLs, and the folder-marker heuristic.
- [references/DKNet.Svc.BlobStorage.Local.md](references/DKNet.Svc.BlobStorage.Local.md) — `LocalBlobService`/
  `LocalDirectoryOptions`, the path-traversal guard, and the no-Docker testing fixture pattern.
- Docs site: https://baoduy.github.io/DKNet/
- NuGet: https://www.nuget.org/packages/DKNet.Svc.BlobStorage.Abstractions (swap the id suffix for `AwsS3`,
  `AzureStorage`, or `Local`)

Files in this skill

  • SKILL.md21.1 KB
  • references/DKNet.Svc.BlobStorage.Abstractions.md25.9 KB
  • references/DKNet.Svc.BlobStorage.AwsS3.md20.5 KB
  • references/DKNet.Svc.BlobStorage.AzureStorage.md25.1 KB
  • references/DKNet.Svc.BlobStorage.Local.md22 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.