DKNet

Configure and operate S3 storage

Part of DKNet.Svc.BlobStorage.AwsS3. Set connection and credential options, then work with the quirks specific to S3’s save, delete, and listing operations.

// Container or EC2/ECS role — leave AccessKey and Secret unset to use the ambient AWS credential chain
var config = new ConfigurationBuilder()
    .AddInMemoryCollection(new Dictionary<string, string?>
    {
        ["BlobService:S3:ConnectionString"] = "http://localhost:9000",
        ["BlobService:S3:BucketName"] = "dev",
        ["BlobService:S3:ForcePathStyle"] = "true",       // required by MinIO and most S3-compatible services
        ["BlobService:S3:DisablePayloadSigning"] = "true" // needed by some S3-compatible endpoints over plain HTTP
    })
    .Build();

When both AccessKey and Secret are non-blank the client uses BasicAWSCredentials; otherwise it falls back to the AWS SDK’s default credential chain (environment variables, shared profile, instance/task role). Which path was taken is logged at Information level on first client construction.

SaveAsync validates the stream, checks whether the key already exists, throws InvalidOperationException on an unflagged conflict, and otherwise uploads a single PutObject and returns the blob's name.

The diagram cannot show the other operations: folder delete pages the prefix with ListObjectsV2 and deletes each page’s keys in parallel with Task.WhenAll per page (not one key at a time — a plain per-key DeleteObjectAsync loop, because this SDK version’s batch-delete API needs a Content-MD5 header that MinIO rejects); a folder delete targeting the bucket root throws ArgumentException instead of deleting everything. ListItemsAsync follows NextContinuationToken across every page, so a prefix with more than 1,000 objects is not truncated.

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

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

GetAsync returns null and CheckExistsAsync returns false when S3 answers 404; any other AmazonS3Exception propagates. GetPublicAccessUrl returns a pre-signed URL valid for expiresFromNow, defaulting to one hour.

⚙️ Configuration reference

S3Options extends BlobServiceOptions, documented on the Abstractions feature page:

Option Type Default Effect
ConnectionString string (required) S3 service endpoint URL, used as the SDK’s ServiceURL; a value that does not start with https switches the client to plain HTTP.
BucketName string (required) Target bucket; created on first use when missing.
AccessKey string? null With Secret, selects BasicAWSCredentials; leave unset to use the ambient AWS credential chain.
Secret string? null Paired with AccessKey.
RegionEndpointName string? "us-east-1" Declared for configuration completeness; the client does not read this value — encode the region in ConnectionString.
ForcePathStyle bool false Set true for MinIO and most S3-compatible services.
DisablePayloadSigning bool false Passed to PutObject; needed by some S3-compatible endpoints over plain HTTP.
S3Options.Name (static) string "BlobService:S3" Configuration section key AddS3BlobService binds from.

🧱 Where it fits

S3BlobService derives from the BlobService base class in Abstractions; application code depends on IBlobService, and only the composition root knows this package exists. Pair it with DKNet.Svc.BlobStorage.Local for tests and local runs when a MinIO container is not wanted.

⚠️ Gotchas & limits