Skip to main content

AWS S3 Provider

Vali-Blob.AWS provides full IStorageProvider, IResumableUploadProvider, and IPresignedUrlProvider implementations backed by Amazon S3 via AWSSDK.S3.


Installation

dotnet add package Vali-Blob.Core
dotnet add package Vali-Blob.AWS

AWSS3Options Reference

OptionTypeRequiredDescription
BucketNamestringYesS3 bucket name
RegionstringYesAWS region code (e.g., us-east-1, eu-west-1)
AccessKeystring?NoAWS access key ID. Leave null to use IAM role / environment credentials
SecretKeystring?NoAWS secret access key. Leave null for IAM role
ServiceUrlstring?NoOverride endpoint URL — use http://localhost:4566 for LocalStack
ForcePathStyleboolNoUse path-style URLs. Required for LocalStack and MinIO. Default: false
ServerSideEncryptionServerSideEncryptionMethod?NoEnable provider-side encryption: AES256 or AWSKMS
SseKmsKeyIdstring?NoKMS key ARN when using AWSKMS encryption

DI Registration

using Vali-Blob.Core;
using Vali-Blob.AWS;

builder.Services
.AddVali-Blob(o => o.DefaultProvider = "aws")
.AddProvider<AWSS3Provider>("aws", opts =>
{
opts.BucketName = builder.Configuration["AWS:BucketName"]!;
opts.Region = builder.Configuration["AWS:Region"]!;
opts.AccessKey = builder.Configuration["AWS:AccessKey"]; // null → use IAM role
opts.SecretKey = builder.Configuration["AWS:SecretKey"];
})
.WithPipeline(p => p
.UseValidation(v =>
{
v.MaxFileSizeBytes = 500_000_000;
v.AllowedExtensions = [".jpg", ".png", ".pdf", ".mp4"];
})
.UseContentTypeDetection()
.UseConflictResolution(ConflictResolution.ReplaceExisting)
);

appsettings.json

{
"AWS": {
"BucketName": "my-app-bucket",
"Region": "us-east-1"
}
}
IAM roles in production

On AWS (EC2, ECS, Lambda, EKS), set AccessKey and SecretKey to null. The SDK resolves credentials automatically from the instance/task role via IMDS. This is more secure than static credentials and eliminates rotation concerns.

Never commit credentials

Use dotnet user-secrets for development credentials. In CI/CD, use environment variables or secrets managers. Never commit AWS credentials to source control.


IAM Policy

Grant your instance/task role the following permissions on the bucket:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:HeadObject",
"s3:CopyObject"
],
"Resource": "arn:aws:s3:::my-app-bucket/*"
},
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": "arn:aws:s3:::my-app-bucket"
}
]
}

For resumable (multipart) uploads, add:

{
"Effect": "Allow",
"Action": [
"s3:CreateMultipartUpload",
"s3:UploadPart",
"s3:CompleteMultipartUpload",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts"
],
"Resource": "arn:aws:s3:::my-app-bucket/*"
}

Presigned URLs

AWSS3Provider implements IPresignedUrlProvider. Use presigned URLs to let browsers and mobile clients upload or download directly to/from S3 — your server never touches the file data:

var provider = factory.Create("aws");

if (provider is IPresignedUrlProvider presigned)
{
// Presigned PUT URL — client uploads directly to S3 for 15 minutes
var uploadUrl = await presigned.GetPresignedUploadUrlAsync(
StoragePath.From("uploads", userId, "avatar.jpg"),
expiresIn: TimeSpan.FromMinutes(15));

// Presigned GET URL — client downloads directly from S3 for 1 hour
var downloadUrl = await presigned.GetPresignedDownloadUrlAsync(
"private/salary-report.pdf",
expiresIn: TimeSpan.FromHours(1));

return Results.Ok(new
{
uploadUrl = uploadUrl.Value,
downloadUrl = downloadUrl.Value
});
}

LocalStack (Local Development)

LocalStack provides a local S3-compatible endpoint for development and testing with no AWS account required:

docker run -d --name localstack -p 4566:4566 localstack/localstack
# Create bucket
aws --endpoint-url=http://localhost:4566 s3 mb s3://dev-bucket

Configure Vali-Blob for LocalStack:

.AddProvider<AWSS3Provider>("aws", opts =>
{
opts.BucketName = "dev-bucket";
opts.Region = "us-east-1";
opts.AccessKey = "test"; // any non-empty value
opts.SecretKey = "test";
opts.ServiceUrl = "http://localhost:4566";
opts.ForcePathStyle = true; // required for LocalStack
})

MinIO (Self-Hosted S3-Compatible)

docker run -d --name minio \
-p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin123 \
minio/minio server /data --console-address ":9001"
.AddProvider<AWSS3Provider>("minio", opts =>
{
opts.BucketName = "my-bucket";
opts.Region = "us-east-1"; // required by SDK but arbitrary for MinIO
opts.AccessKey = "minioadmin";
opts.SecretKey = "minioadmin123";
opts.ServiceUrl = "http://localhost:9000";
opts.ForcePathStyle = true;
})

Multiple Regions

Configure multiple named providers for multi-region deployments:

builder.Services
.AddVali-Blob(o => o.DefaultProvider = "aws-us")
.AddProvider<AWSS3Provider>("aws-us", opts =>
{
opts.BucketName = "my-app-us";
opts.Region = "us-east-1";
})
.AddProvider<AWSS3Provider>("aws-eu", opts =>
{
opts.BucketName = "my-app-eu";
opts.Region = "eu-west-1";
});

// Resolve by name
var usProvider = factory.Create("aws-us");
var euProvider = factory.Create("aws-eu");

Server-Side Encryption

Use provider-side encryption in addition to Vali-Blob's application-layer encryption:

opts.ServerSideEncryption = ServerSideEncryptionMethod.AES256;   // SSE-S3
// or AWS KMS:
opts.ServerSideEncryption = ServerSideEncryptionMethod.AWSKMS;
opts.SseKmsKeyId = "arn:aws:kms:us-east-1:123456789:key/your-key-id";
Defense in depth

Vali-Blob's EncryptionMiddleware encrypts at the application layer before bytes leave your process. S3 server-side encryption (SSE-S3 or SSE-KMS) adds a second encryption layer at the storage layer. Using both provides defense-in-depth for sensitive data.


Supported Operations

OperationSupportedNotes
UploadAsyncYesSingle PUT or multipart
DownloadAsyncYesIncluding byte range (partial content)
DeleteAsyncYes
DeleteFolderAsyncYesBatch delete via ListObjectsV2 + DeleteObjects
ExistsAsyncYesUses HeadObject
CopyAsyncYesServer-side copy
GetMetadataAsyncYesHeadObject + custom metadata
SetMetadataAsyncYesCopy-in-place with new metadata
ListFilesAsyncYesListObjectsV2 with auto-pagination
ListFoldersAsyncYesListObjectsV2 with delimiter
GetUrlAsyncYesPublic or presigned URL
StartResumableUploadAsyncYesS3 CreateMultipartUpload
UploadChunkAsyncYesS3 UploadPart
CompleteResumableUploadAsyncYesS3 CompleteMultipartUpload
AbortResumableUploadAsyncYesS3 AbortMultipartUpload
GetPresignedUploadUrlAsyncYesPre-signed PUT URL
GetPresignedDownloadUrlAsyncYesPre-signed GET URL