Compendium.Adapters.S3
1.0.0-preview.0
dotnet add package Compendium.Adapters.S3 --version 1.0.0-preview.0
NuGet\Install-Package Compendium.Adapters.S3 -Version 1.0.0-preview.0
<PackageReference Include="Compendium.Adapters.S3" Version="1.0.0-preview.0" />
<PackageVersion Include="Compendium.Adapters.S3" Version="1.0.0-preview.0" />
<PackageReference Include="Compendium.Adapters.S3" />
paket add Compendium.Adapters.S3 --version 1.0.0-preview.0
#r "nuget: Compendium.Adapters.S3, 1.0.0-preview.0"
#:package Compendium.Adapters.S3@1.0.0-preview.0
#addin nuget:?package=Compendium.Adapters.S3&version=1.0.0-preview.0&prerelease
#tool nuget:?package=Compendium.Adapters.S3&version=1.0.0-preview.0&prerelease
Compendium.Adapters.S3
S3-compatible object-store adapter for the Compendium framework. One adapter, five providers:
- AWS S3 (default)
- Cloudflare R2 (custom endpoint)
- MinIO (custom endpoint + path-style)
- Backblaze B2 (S3-compatible API)
- Wasabi
Implements the IObjectStore port with tenant-scoped keys, streaming uploads, multipart, server-side encryption, and presigned URLs. First-ever consumer of the storage port shipped from Compendium.Abstractions.Storage (currently embedded in this package until the abstraction is published — see src/Compendium.Adapters.S3/Abstractions/IObjectStore.cs).
Quick start
dotnet add package Compendium.Adapters.S3
using Compendium.Adapters.S3.DependencyInjection;
var services = new ServiceCollection();
services.AddCompendiumMultitenancy(); // your tenant resolution strategy
services.AddCompendiumS3(o =>
{
o.Bucket = "invoices";
o.Region = "us-east-1";
// Optional — omit to use the SDK's default credential chain.
o.AccessKey = "...";
o.SecretKey = "...";
});
Inject IObjectStore anywhere:
public sealed class InvoiceService(IObjectStore store)
{
public async Task UploadAsync(string key, Stream content, CancellationToken ct)
{
var result = await store.PutAsync(key, content, "application/pdf", cancellationToken: ct);
if (result.IsFailure)
{
// Result.Error.Type is NotFound / Conflict / Forbidden / ... etc.
}
}
}
Every key is automatically prefixed with the current tenant id (resolved from ITenantContext). Cross-tenant access is impossible by construction.
Options
| Property | Type | Default | Notes |
|---|---|---|---|
Bucket |
string |
required | Default bucket. |
Region |
string? |
null |
AWS region, e.g. us-east-1. Required when no ServiceUrl. |
ServiceUrl |
string? |
null |
Custom endpoint for R2/MinIO/B2/Wasabi. |
ForcePathStyle |
bool |
false |
Required for MinIO. |
AccessKey |
string? |
null |
Omit to use the SDK credential chain (IAM role, env, profile). |
SecretKey |
string? |
null |
See above. |
MultipartThresholdBytes |
long |
8 MiB |
Uploads above this go multipart (5 MiB part size). |
DefaultPresignedUrlExpiry |
TimeSpan |
15m |
Default lifetime for presigned URLs. |
ServerSideEncryption |
enum |
None |
None / Aes256 / AwsKms. |
KmsKeyId |
string? |
null |
Required when ServerSideEncryption = AwsKms. |
Bind from IConfiguration under the canonical section:
{
"Compendium": {
"Adapters": {
"S3": {
"Bucket": "invoices",
"Region": "us-east-1",
"ServerSideEncryption": "Aes256"
}
}
}
}
services.AddCompendiumS3(configuration);
Provider configurations
AWS S3 (default)
services.AddCompendiumS3(o =>
{
o.Bucket = "prod-invoices";
o.Region = "us-east-1";
// No credentials → uses IAM role / EC2 instance profile / EKS pod identity.
});
Cloudflare R2
services.AddCompendiumS3(o =>
{
o.Bucket = "invoices";
o.ServiceUrl = "https://<account-id>.r2.cloudflarestorage.com";
o.AccessKey = "<r2-access-key>";
o.SecretKey = "<r2-secret-key>";
// R2 doesn't use AWS regions, but the SDK requires *some* region for signing.
o.Region = "auto";
});
MinIO (self-hosted / local)
services.AddCompendiumS3(o =>
{
o.Bucket = "invoices";
o.ServiceUrl = "http://minio.internal:9000";
o.ForcePathStyle = true; // MANDATORY for MinIO
o.AccessKey = "minio";
o.SecretKey = "<password>";
o.Region = "us-east-1";
});
Backblaze B2 (S3-compatible)
services.AddCompendiumS3(o =>
{
o.Bucket = "invoices";
o.ServiceUrl = "https://s3.us-west-002.backblazeb2.com";
o.AccessKey = "<b2-keyId>";
o.SecretKey = "<b2-applicationKey>";
o.Region = "us-west-002";
});
Wasabi
services.AddCompendiumS3(o =>
{
o.Bucket = "invoices";
o.ServiceUrl = "https://s3.us-east-1.wasabisys.com";
o.AccessKey = "<wasabi-access-key>";
o.SecretKey = "<wasabi-secret-key>";
o.Region = "us-east-1";
});
Tenant isolation
Every operation on IObjectStore prefixes the supplied key with {tenantId}/:
store.PutAsync("invoice.pdf", ...)→tenant-a/invoice.pdfstore.ListAsync("invoices/")→ liststenant-a/invoices/*, nevertenant-b/...
The tenant id is validated against the same regex used by the PostgreSQL adapter — ^[a-zA-Z0-9_-]+$, max 255 chars. Malformed tenant ids and path-traversal segments (..) are rejected with Error.Forbidden.
Sample
See samples/01-tenant-invoice-roundtrip — uploads an invoice and generates a 1-hour presigned download URL.
# Start MinIO :
docker run -p 9000:9000 -e MINIO_ROOT_USER=minio -e MINIO_ROOT_PASSWORD=minio12345 \
minio/minio:latest server /data
# Create the bucket once via the MinIO console or `mc mb`.
dotnet run --project samples/01-tenant-invoice-roundtrip
Production checklist
- TLS everywhere. Never set
ServiceUrltohttp://outside dev. - IAM roles, not access keys. Omit
AccessKey/SecretKeyin production and rely on the SDK credential chain — env vars on Kubernetes (via IRSA / pod identity), instance profiles on EC2, OIDC on GitHub Actions. - Bucket policy. Restrict the principal to the IAM role used by the service ; deny anything else. Block public access at the account level.
- Server-side encryption. Set
ServerSideEncryption = Aes256minimum. UseAwsKmswith a customer-managed key for regulated workloads. - Lifecycle policies. Configure via the AWS console / Terraform /
awscli— out of scope for the adapter. - Versioning + object lock. Enable on the bucket if compliance requires immutability. Out of scope for the adapter.
- Multi-region replication. Configure at bucket level. The adapter only talks to one endpoint at a time.
- Presigned URL TTL. Default 15 minutes ; never grant > 1 hour for sensitive data. AWS hard-limits SigV4 URLs to 7 days.
Coverage
CI gate : ≥ 90 % line coverage on the unit-testable surface. Integration tests are excluded from the gate (Docker may be unavailable in CI environments without a daemon).
License
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- AWSSDK.S3 (>= 3.7.511.7)
- Compendium.Abstractions (>= 1.0.1)
- Compendium.Core (>= 1.0.1)
- Compendium.Multitenancy (>= 1.0.1)
- Microsoft.Extensions.Configuration.Abstractions (>= 9.0.16)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.16)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.16)
- Microsoft.Extensions.Options (>= 9.0.16)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.16)
- Microsoft.Extensions.Options.DataAnnotations (>= 9.0.16)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-preview.0 | 31 | 5/17/2026 |