RepletoryLib.FileStorage.Aws 1.0.0

dotnet add package RepletoryLib.FileStorage.Aws --version 1.0.0
                    
NuGet\Install-Package RepletoryLib.FileStorage.Aws -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RepletoryLib.FileStorage.Aws" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RepletoryLib.FileStorage.Aws" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="RepletoryLib.FileStorage.Aws" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RepletoryLib.FileStorage.Aws --version 1.0.0
                    
#r "nuget: RepletoryLib.FileStorage.Aws, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RepletoryLib.FileStorage.Aws@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RepletoryLib.FileStorage.Aws&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=RepletoryLib.FileStorage.Aws&version=1.0.0
                    
Install as a Cake Tool

RepletoryLib.FileStorage.Aws

AWS S3 file storage implementation with presigned URLs, bulk operations, multi-tenant path strategies, and S3-compatible service support (LocalStack, MinIO).

Part of the RepletoryLib ecosystem -- standalone, reusable .NET 10 libraries with zero business logic.

NuGet .NET 10 License: MIT


Overview

RepletoryLib.FileStorage.Aws provides a full AWS S3 implementation of IFileStorageService from the FileStorage.Abstractions package. It supports uploading (single and bulk), downloading (stream and byte array), deleting (single and bulk), copying, moving, metadata retrieval, paginated listing, presigned URL generation, and public URL construction.

The package includes S3FilePathStrategy for generating organized S3 object keys using configurable path templates with multi-tenant and date-based partitioning, and automatic content type inference from file extensions.

Key Features

  • S3FileStorageService -- Full IFileStorageService implementation backed by AWS S3
  • Upload -- Single and bulk uploads with automatic content type inference, ACL mapping, and custom metadata
  • Download -- Stream-based and byte array downloads
  • Delete -- Single and bulk deletes using S3 multi-object delete API
  • Copy & Move -- Server-side copy; move is copy + delete
  • Presigned URLs -- Configurable expiry and HTTP method (GET, PUT, DELETE, HEAD)
  • Public URLs -- Virtual-hosted-style and path-style URL construction
  • S3FilePathStrategy -- Token-based path generation with {tenant}, {year}, {month}, {day}, {filename}
  • S3-compatible -- Works with LocalStack, MinIO, and other S3-compatible services via ServiceUrl and ForcePathStyle
  • Visibility control -- Automatic S3CannedACL mapping from FileVisibility enum
  • Content type inference -- 30+ MIME type mappings for common file extensions

Installation

dotnet add package RepletoryLib.FileStorage.Aws

Or add to your .csproj:

<PackageReference Include="RepletoryLib.FileStorage.Aws" Version="1.0.0" />

Note: RepletoryLib packages are published to a local BaGet feed. See the main repository README for feed configuration.

Dependencies

Package Type
RepletoryLib.FileStorage.Abstractions RepletoryLib
AWSSDK.S3 NuGet (3.7.410.1)
Microsoft.Extensions.Configuration.Abstractions NuGet (10.0.0)
Microsoft.Extensions.Configuration.Binder NuGet (10.0.0)
Microsoft.Extensions.DependencyInjection.Abstractions NuGet (10.0.0)
Microsoft.Extensions.Logging.Abstractions NuGet (10.0.0)
Microsoft.Extensions.Options NuGet (10.0.0)
Microsoft.Extensions.Options.ConfigurationExtensions NuGet (10.0.0)

Prerequisites

  • AWS account with an S3 bucket created, or an S3-compatible service (LocalStack, MinIO)
  • AWS credentials configured via appsettings.json, environment variables, or IAM role

Quick Start

using RepletoryLib.FileStorage.Aws;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRepletoryAwsFileStorage(builder.Configuration);
{
  "FileStorage": {
    "MaxFileSizeBytes": 104857600,
    "AllowedExtensions": [],
    "DefaultVisibility": "Private",
    "PathTemplate": "{tenant}/{year}/{month}/{filename}",
    "PreserveOriginalFileName": true,
    "AwsS3": {
      "BucketName": "my-app-files",
      "Region": "af-south-1",
      "AccessKey": "AKIAIOSFODNN7EXAMPLE",
      "SecretKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
      "PresignedUrlExpiryMinutes": 60
    }
  }
}

Configuration

AwsS3Options

Property Type Default Description
BucketName string "" Name of the S3 bucket
Region string "af-south-1" AWS region for the S3 bucket
AccessKey string? null AWS access key (uses default credential chain if null)
SecretKey string? null AWS secret key (uses default credential chain if null)
ServiceUrl string? null Custom endpoint for S3-compatible services (LocalStack, MinIO)
ForcePathStyle bool false Use path-style addressing (required for some S3-compatible services)
PresignedUrlExpiryMinutes int 60 Default presigned URL expiry in minutes

Section name: "FileStorage:AwsS3"

FileStorageOptions (shared)

Property Type Default Description
MaxFileSizeBytes long 104857600 (100 MB) Maximum allowed file size in bytes
AllowedExtensions List<string> [] (all allowed) Allowed file extensions
DefaultVisibility FileVisibility Private Default access visibility for uploads
PathTemplate string "{tenant}/{year}/{month}/{filename}" Token-based path template for S3 object keys
PreserveOriginalFileName bool true When false, generates GUID-based file names

Section name: "FileStorage"

LocalStack / MinIO Configuration

{
  "FileStorage": {
    "AwsS3": {
      "BucketName": "test-bucket",
      "Region": "us-east-1",
      "ServiceUrl": "http://localhost:4566",
      "ForcePathStyle": true,
      "AccessKey": "test",
      "SecretKey": "test"
    }
  }
}

Usage Examples

Upload a File

using RepletoryLib.FileStorage.Abstractions.Enums;
using RepletoryLib.FileStorage.Abstractions.Interfaces;
using RepletoryLib.FileStorage.Abstractions.Models;

public class DocumentService
{
    private readonly IFileStorageService _storage;

    public DocumentService(IFileStorageService storage) => _storage = storage;

    public async Task<FileUploadResult> UploadAsync(Stream content, string fileName, Guid tenantId)
    {
        var request = new FileUploadRequest
        {
            Content = content,
            FileName = fileName,
            Folder = "documents",
            Visibility = FileVisibility.Private,
            Metadata = new Dictionary<string, string>
            {
                ["tenant-id"] = tenantId.ToString(),
                ["uploaded-by"] = "document-service"
            }
        };

        var result = await _storage.UploadAsync(request);

        if (!result.Success)
        {
            throw new InvalidOperationException($"Upload failed: {result.Error}");
        }

        return result;
        // result.FileKey = "documents/default/2026/03/report.pdf"
    }
}

Upload Public Images

public async Task<string> UploadAvatarAsync(Stream imageStream, string fileName)
{
    var result = await _storage.UploadAsync(new FileUploadRequest
    {
        Content = imageStream,
        FileName = fileName,
        ContentType = "image/jpeg",
        Folder = "avatars",
        Visibility = FileVisibility.Public
    });

    // Public files get a URL immediately
    // e.g., "https://my-app-files.s3.af-south-1.amazonaws.com/avatars/default/2026/03/photo.jpg"
    return result.Url;
}

Bulk Upload

public async Task<BulkUploadResult> UploadBatchAsync(List<IFormFile> files)
{
    var requests = files.Select(f => new FileUploadRequest
    {
        Content = f.OpenReadStream(),
        FileName = f.FileName,
        ContentType = f.ContentType,
        Folder = "batch-imports"
    });

    var result = await _storage.UploadBulkAsync(requests);

    // Partial success -- individual files may fail independently
    if (result.FailureCount > 0)
    {
        foreach (var failed in result.Results.Where(r => !r.Success))
        {
            _logger.LogWarning("Upload failed: {Error}", failed.Error);
        }
    }

    return result;
}

Download Files

// Download as stream (caller must dispose)
public async Task<Stream> DownloadAsync(string fileKey)
{
    return await _storage.DownloadAsync(fileKey);
}

// Download as byte array
public async Task<byte[]> DownloadBytesAsync(string fileKey)
{
    return await _storage.DownloadBytesAsync(fileKey);
}

Copy, Move, and Delete

// Server-side copy (no data transfer through your app)
await _storage.CopyAsync(
    "documents/default/2026/03/report.pdf",
    "backups/default/2026/03/report.pdf");

// Move (copy + delete source)
await _storage.MoveAsync(
    "temp/uploads/draft.docx",
    "documents/default/2026/03/final.docx");

// Delete single file
await _storage.DeleteAsync("temp/uploads/draft.docx");

// Bulk delete (uses S3 multi-object delete API)
await _storage.DeleteBulkAsync(new[]
{
    "temp/file1.pdf",
    "temp/file2.pdf",
    "temp/file3.pdf"
});

Get Metadata

var metadata = await _storage.GetMetadataAsync("documents/default/2026/03/report.pdf");

Console.WriteLine($"File: {metadata.FileName}");       // "report.pdf"
Console.WriteLine($"Size: {metadata.SizeBytes} bytes"); // 1048576
Console.WriteLine($"Type: {metadata.ContentType}");     // "application/pdf"
Console.WriteLine($"Created: {metadata.CreatedAt}");    // 2026-03-15T10:30:00Z
Console.WriteLine($"Custom: {metadata.Metadata["uploaded-by"]}"); // "document-service"

List Files with Pagination

var request = new FileListRequest
{
    Prefix = "documents/default/2026/",
    MaxResults = 100
};

var result = await _storage.ListAsync(request);

foreach (var file in result.Files)
{
    Console.WriteLine($"{file.FileKey} - {file.SizeBytes} bytes");
}

// Continue paginating
while (result.IsTruncated)
{
    request.ContinuationToken = result.ContinuationToken;
    result = await _storage.ListAsync(request);

    foreach (var file in result.Files)
    {
        Console.WriteLine($"{file.FileKey} - {file.SizeBytes} bytes");
    }
}

Generate Presigned URLs

// Presigned download URL (2 hours)
var downloadUrl = await _storage.GetPresignedUrlAsync(new PresignedUrlRequest
{
    FileKey = "documents/default/2026/03/report.pdf",
    Expiry = TimeSpan.FromHours(2),
    Method = HttpMethod.Get
});

// Presigned upload URL (15 minutes)
var uploadUrl = await _storage.GetPresignedUrlAsync(new PresignedUrlRequest
{
    FileKey = "uploads/new-document.pdf",
    Expiry = TimeSpan.FromMinutes(15),
    Method = HttpMethod.Put
});

// Public URL for publicly-visible files
var publicUrl = await _storage.GetPublicUrlAsync("avatars/default/2026/03/photo.jpg");
// "https://my-app-files.s3.af-south-1.amazonaws.com/avatars/default/2026/03/photo.jpg"

Check Existence

bool exists = await _storage.ExistsAsync("documents/default/2026/03/report.pdf");

if (!exists)
{
    throw new FileNotFoundException("Document not found in S3");
}

API Reference

ServiceCollectionExtensions

Method Returns Description
AddRepletoryAwsFileStorage(configuration) IServiceCollection Registers IAmazonS3, IFilePathStrategy (singleton), and IFileStorageService (scoped)

S3FileStorageService (implements IFileStorageService)

Method Returns Description
UploadAsync(request, ct) Task<FileUploadResult> Upload a file to S3 with ACL, content type, and custom metadata
UploadBulkAsync(requests, ct) Task<BulkUploadResult> Upload multiple files sequentially with partial-success semantics
DownloadAsync(fileKey, ct) Task<Stream> Download a file as a stream from S3
DownloadBytesAsync(fileKey, ct) Task<byte[]> Download a file as a byte array from S3
DeleteAsync(fileKey, ct) Task Delete a single object from S3
DeleteBulkAsync(fileKeys, ct) Task Delete multiple objects using S3 multi-object delete
ExistsAsync(fileKey, ct) Task<bool> Check if an object exists via HEAD request
CopyAsync(sourceKey, destinationKey, ct) Task Server-side copy within the same bucket
MoveAsync(sourceKey, destinationKey, ct) Task Copy then delete (move within same bucket)
GetMetadataAsync(fileKey, ct) Task<FileMetadata> Retrieve object metadata via HEAD request
ListAsync(request, ct) Task<FileListResult> List objects with prefix, delimiter, and continuation token
GetPresignedUrlAsync(request, ct) Task<string> Generate a presigned URL for GET, PUT, DELETE, or HEAD
GetPublicUrlAsync(fileKey, ct) Task<string> Build a public URL (virtual-hosted or path-style)

S3FilePathStrategy (implements IFilePathStrategy)

Method Returns Description
GeneratePath(context) string Generate an S3 object key by replacing tokens in the path template

Registered Services

Interface Implementation Lifetime
IAmazonS3 AmazonS3Client Singleton
IFilePathStrategy S3FilePathStrategy Singleton
IFileStorageService S3FileStorageService Scoped

Integration with Other RepletoryLib Packages

Package Relationship
RepletoryLib.FileStorage.Abstractions Direct dependency -- IFileStorageService, IFilePathStrategy, all models and options
RepletoryLib.FileStorage.Local Alternative provider -- swap to local filesystem for development
RepletoryLib.Common Transitive dependency via Abstractions
RepletoryLib.Api Use IFileStorageService in API controllers for file upload/download endpoints

Testing

using NSubstitute;
using RepletoryLib.FileStorage.Abstractions.Interfaces;
using RepletoryLib.FileStorage.Abstractions.Models;

public class DocumentServiceTests
{
    [Fact]
    public async Task UploadAsync_calls_storage_and_returns_file_key()
    {
        // Arrange
        var storage = Substitute.For<IFileStorageService>();
        storage.UploadAsync(Arg.Any<FileUploadRequest>(), Arg.Any<CancellationToken>())
            .Returns(new FileUploadResult
            {
                Success = true,
                FileKey = "documents/default/2026/03/report.pdf",
                Url = "https://my-bucket.s3.af-south-1.amazonaws.com/documents/default/2026/03/report.pdf"
            });

        var service = new DocumentService(storage);

        // Act
        using var stream = new MemoryStream("test content"u8.ToArray());
        var result = await service.UploadAsync(stream, "report.pdf", Guid.NewGuid());

        // Assert
        result.Success.Should().BeTrue();
        result.FileKey.Should().Contain("report.pdf");
        await storage.Received(1).UploadAsync(
            Arg.Is<FileUploadRequest>(r =>
                r.FileName == "report.pdf" &&
                r.Folder == "documents"),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task ExistsAsync_returns_false_for_missing_file()
    {
        // Arrange
        var storage = Substitute.For<IFileStorageService>();
        storage.ExistsAsync("nonexistent/file.pdf", Arg.Any<CancellationToken>())
            .Returns(false);

        // Act
        var exists = await storage.ExistsAsync("nonexistent/file.pdf");

        // Assert
        exists.Should().BeFalse();
    }
}

Troubleshooting

Issue Solution
AmazonS3Exception: Access Denied Verify AccessKey/SecretKey in configuration, or ensure the IAM role has s3:PutObject, s3:GetObject, s3:DeleteObject permissions
AmazonS3Exception: NoSuchBucket Ensure BucketName in AwsS3Options matches an existing bucket in the configured Region
Presigned URLs return 403 Check that the URL has not expired; increase Expiry in PresignedUrlRequest or PresignedUrlExpiryMinutes in options
Public URLs return 403 Ensure the file was uploaded with FileVisibility.Public and the bucket policy allows public reads
LocalStack/MinIO connection refused Set ServiceUrl to the correct endpoint (e.g., http://localhost:4566) and ForcePathStyle to true
Content type is application/octet-stream Set ContentType explicitly on FileUploadRequest, or ensure the file has a recognized extension
Path template produces unexpected keys Verify PathTemplate tokens in FileStorageOptions; supported tokens: {tenant}, {year}, {month}, {day}, {filename}
Bulk upload partially fails Check BulkUploadResult.Results for per-file Error messages; individual failures do not affect other uploads

License

This project is licensed under the MIT License.

Copyright (c) 2024-2026 Repletory.


For complete documentation, infrastructure setup, and configuration reference, see the RepletoryLib main repository.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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 74 3/2/2026