GM.FileStorage.S3 1.1.0

dotnet add package GM.FileStorage.S3 --version 1.1.0
                    
NuGet\Install-Package GM.FileStorage.S3 -Version 1.1.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="GM.FileStorage.S3" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GM.FileStorage.S3" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="GM.FileStorage.S3" />
                    
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 GM.FileStorage.S3 --version 1.1.0
                    
#r "nuget: GM.FileStorage.S3, 1.1.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 GM.FileStorage.S3@1.1.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=GM.FileStorage.S3&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=GM.FileStorage.S3&version=1.1.0
                    
Install as a Cake Tool

<p align="center"> <img src="https://raw.githubusercontent.com/gmetskhvarishvili/GM.FileStorage/master/icon.png" alt="GM.FileStorage" width="140" height="140" /> </p>

GM.FileStorage

CI NuGet License: MIT

A provider-agnostic file/blob storage abstraction for .NET. Depend on one interface — IFileStorageService — with streaming upload/download (no loading large files into memory), metadata + SHA-256 checksum validation, presigned URLs, and a pluggable key/path strategy (e.g. tenant/user-scoped folders). The backend is chosen by configuration, so switching disk → S3 → Azure Blob is a config change, not a code change. Targets .NET 10.

Packages

Core has no provider dependencies; each backend is its own package (like GM.Messaging / its transports). They version and release in lockstep:

Package Backend Status
GM.FileStorage core abstraction + config-driven selection ✅ shipped
GM.FileStorage.Local local disk (dev / tests) ✅ shipped
GM.FileStorage.S3 AWS S3 / S3-compatible (MinIO) ✅ shipped
GM.FileStorage.AzureBlob Azure Blob Storage ✅ shipped
dotnet add package GM.FileStorage
dotnet add package GM.FileStorage.Local

The interface

public interface IFileStorageService
{
    Task<FileMetadata>       UploadAsync(FileUploadRequest request, CancellationToken ct = default);
    Task<FileDownloadResult> DownloadAsync(string key, CancellationToken ct = default);
    Task<bool>               ExistsAsync(string key, CancellationToken ct = default);
    Task                     DeleteAsync(string key, CancellationToken ct = default);
    Task<Uri>                GetPresignedUrlAsync(string key, PresignedUrlRequest request, CancellationToken ct = default);
}

No provider types leak into the surface — just streams, keys and the GM types:

  • FileUploadRequestStream Content, FileName, ContentType, optional Key, Visibility (Private/Public), custom Metadata, and an optional ExpectedChecksum.
  • FileMetadataKey, FileName, ContentType, Size, Checksum, Visibility, LastModifiedUtc, Metadata.
  • FileDownloadResult — a disposable Stream Content + FileMetadata (streamed, not buffered).
  • PresignedUrlRequestExpiry + Operation (Read/Write).
  • IFileKeyStrategy — decides the storage key when a request doesn't set one.

Quick start

using GM.FileStorage;
using GM.FileStorage.Local;

builder.Services.AddGMFileStorage(builder.Configuration);   // reads FileStorage:Provider
builder.Services.AddGMLocalFileStorage(builder.Configuration); // contributes the "Local" backend
// appsettings.json
"FileStorage": {
  "Provider": "Local",                 // swap to "S3" / "AzureBlob" once those packages are added
  "Local": { "RootPath": "App_Data/files", "PublicBaseUrl": "http://localhost:5000/files" }
}

Then inject IFileStorageService — the same code works on any backend:

public class KycDocuments(IFileStorageService storage)
{
    public async Task<string> StoreAsync(Stream file, string fileName, string tenantId, string userId)
    {
        var meta = await storage.UploadAsync(new FileUploadRequest(file, fileName, "application/pdf")
        {
            Visibility = FileVisibility.Private,
            Metadata = new Dictionary<string, string> { ["tenantId"] = tenantId, ["userId"] = userId },
            // ExpectedChecksum = "<sha256>",  // reject if the bytes don't match end to end
        });
        return meta.Key;   // e.g. tenants/acme/users/u1/9f1c…a3.pdf with a tenant/user key strategy
    }

    public async Task<Uri> LinkAsync(string key) =>
        await storage.GetPresignedUrlAsync(key, new PresignedUrlRequest { Expiry = TimeSpan.FromMinutes(10) });
}

Config-driven provider selection

AddGMFileStorage resolves IFileStorageService to the backend named by FileStorage:Provider. Each provider package registers itself as a keyed service under its name ("Local", "S3", "AzureBlob"). Reference the providers you might use, call their AddGM… methods, and pick between them purely in config — no code change to swap.

Pluggable key/path strategy

The default key is date-partitioned and collision-free (2026/08/02/{guid}.pdf). For the KYC flow, replace IFileKeyStrategy to scope by tenant/user:

public sealed class TenantKeyStrategy : IFileKeyStrategy
{
    public string GenerateKey(FileKeyContext ctx) =>
        $"tenants/{ctx.Metadata["tenantId"]}/users/{ctx.Metadata["userId"]}/{Guid.NewGuid():N}{Path.GetExtension(ctx.FileName)}";
}

builder.Services.AddSingleton<IFileKeyStrategy, TenantKeyStrategy>();

Streaming & integrity

Uploads and downloads stream through an 80 KiB buffer, so a large KYC scan or image is never fully in memory. Every upload's SHA-256 is computed in the same pass; set FileUploadRequest.ExpectedChecksum and a mismatch is rejected with ChecksumMismatchException and nothing is stored.

Do you need GM.DistributedLock here? (flagged, not assumed)

Core deliberately does not depend on GM.DistributedLock. For the common operations it isn't needed: object stores are last-write-wins per key, and unique keys (the default strategy) mean concurrent uploads don't collide.

A lock is worth it for one specific case: serializing writes to the same logical key — e.g. "only one upload for kyc/{userId}/passport may be in flight," or a read-modify-write on a file. That's an application-level concern, so rather than bake it into every consumer, wrap the upload:

await using var handle = await locks.AcquireAsync($"file:{key}", TimeSpan.FromSeconds(30),
    wait: TimeSpan.FromSeconds(5), retryInterval: TimeSpan.FromMilliseconds(100));
await storage.UploadAsync(request with { Key = key });

If this pattern turns out to be pervasive, a thin optional GM.FileStorage.DistributedLock decorator (IFileStorageService → lock-per-key → inner service) could add it without touching core or any provider. Recommendation: keep it out of core; add the decorator only if the need is real.

S3 and Azure providers

Both map the same IFileStorageService onto their SDK and register a keyed backend, so an app moves from LocalS3AzureBlob by configuration alone — no code change.

S3 / MinIO (GM.FileStorage.S3)

builder.Services.AddGMFileStorage(builder.Configuration);
builder.Services.AddGMS3FileStorage(builder.Configuration);   // FileStorage:S3
"FileStorage": {
  "Provider": "S3",
  "S3": {
    "BucketName": "kyc-docs",
    "Region": "eu-central-1",
    // MinIO / S3-compatible instead of AWS:
    "ServiceUrl": "http://localhost:9000", "ForcePathStyle": true,
    "AccessKey": "…", "SecretKey": "…"      // omit to use the default AWS credential chain
  }
}

PutObject/GetObject streaming, GetPreSignedURL for presigned read/write, object metadata (x-amz-meta-gm-* for filename/checksum/visibility), and CannedACL for Public/Private.

Azure Blob (GM.FileStorage.AzureBlob)

builder.Services.AddGMFileStorage(builder.Configuration);
builder.Services.AddGMAzureBlobFileStorage(builder.Configuration);   // FileStorage:AzureBlob
"FileStorage": {
  "Provider": "AzureBlob",
  "AzureBlob": { "ConnectionString": "…", "ContainerName": "kyc" }
}

Block-blob streaming, SAS URLs via GenerateSasUri, and blob metadata. Note: Azure public access is container-scoped, so per-blob Visibility is advisory — use the SAS URL for time-limited access.

Repository layout

GM.FileStorage/            # IFileStorageService, requests/results, key strategy, options, DI selector
GM.FileStorage.Local/      # disk provider (streaming + SHA-256 + sidecar metadata)
GM.FileStorage.S3/         # AWS S3 / MinIO provider
GM.FileStorage.AzureBlob/  # Azure Blob Storage provider
tests/GM.FileStorage.Tests/  # xUnit tests

Building & testing

dotnet build -c Release
dotnet test  -c Release

Releasing

Versioning is automated from Conventional Commits — see CONTRIBUTING.md. All packages share one version (Directory.Build.props) and publish together to nuget.org on each release.

License

MIT — see LICENSE.

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.1.0 109 8/2/2026