GM.FileStorage
1.1.0
dotnet add package GM.FileStorage --version 1.1.0
NuGet\Install-Package GM.FileStorage -Version 1.1.0
<PackageReference Include="GM.FileStorage" Version="1.1.0" />
<PackageVersion Include="GM.FileStorage" Version="1.1.0" />
<PackageReference Include="GM.FileStorage" />
paket add GM.FileStorage --version 1.1.0
#r "nuget: GM.FileStorage, 1.1.0"
#:package GM.FileStorage@1.1.0
#addin nuget:?package=GM.FileStorage&version=1.1.0
#tool nuget:?package=GM.FileStorage&version=1.1.0
<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
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:
FileUploadRequest—Stream Content,FileName,ContentType, optionalKey,Visibility(Private/Public), customMetadata, and an optionalExpectedChecksum.FileMetadata—Key,FileName,ContentType,Size,Checksum,Visibility,LastModifiedUtc,Metadata.FileDownloadResult— a disposableStream Content+FileMetadata(streamed, not buffered).PresignedUrlRequest—Expiry+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 Local → S3 → AzureBlob 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 | Versions 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. |
-
net10.0
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
NuGet packages (5)
Showing the top 5 NuGet packages that depend on GM.FileStorage:
| Package | Downloads |
|---|---|
|
GM.FileStorage.Local
Local disk file-storage provider for GM.FileStorage — streams files to a root directory with SHA-256 checksum validation and sidecar metadata. Ideal for development and tests. Register with AddGMLocalFileStorage() and select it via FileStorage:Provider = "Local". |
|
|
GM.FileStorage.S3
AWS S3 (and S3-compatible, e.g. MinIO) provider for GM.FileStorage: streaming Put/Get, presigned read/write URLs, object metadata, and canned-ACL visibility. Register with AddGMS3FileStorage() and select it via FileStorage:Provider = "S3". |
|
|
GM.FileStorage.AzureBlob
Azure Blob Storage provider for GM.FileStorage: streaming block-blob upload/download, SAS URLs, and blob metadata. Register with AddGMAzureBlobFileStorage() and select it via FileStorage:Provider = "AzureBlob". |
|
|
GM.Testing.Fakes
In-memory fakes of the core GM.* infrastructure abstractions, so unit tests run without real infra: FakeCacheService (ICacheService, with inspection), FakeDistributedLock (IDistributedLock, real in-process mutual exclusion so lock-guarded code can be tested, or an always-acquire mode), and FakeFileStorageService (IFileStorageService backed by byte arrays, honouring checksums and not-found semantics). Each exposes assertion-friendly inspection (keys, counts, stored bytes). Register with AddGMTestingFakes() or drop the instance into a GmWebApplicationFactory override. Test-only (DevelopmentDependency). |
|
|
GM.KYC
Provider-agnostic know-your-customer verification for the GM.* ecosystem. IKycVerificationService (InitiateVerificationAsync / SubmitDocumentAsync / GetVerificationStatusAsync / SubmitLivenessCheckAsync) orchestrates a vendor behind the IKycProvider SPI: document images are normalized and EXIF/GPS-stripped via GM.Documents.Images and persisted via GM.FileStorage before submission, results and attempts flow to an audit sink, and the vendor call is isolated so a second provider is a config change, not a consumer change. Identomat is the provider today (GM.KYC.Identomat). Wire up with AddGMKyc().AddIdentomat(). |
GitHub repositories
This package is not used by any popular GitHub repositories.