Skip to content

Pragmatic.Storage

Provider-agnostic file storage for the Pragmatic.Design ecosystem.

Every non-trivial app stores files — avatars, PDFs, CSV imports — and the backend changes by environment: local disk in dev, Azure Blob in staging, S3 in production. Without an abstraction, domain code couples to a cloud SDK: switching providers means rewriting business logic, running locally needs an emulator, and unit tests must mock the whole SDK surface.

// Without Pragmatic.Storage: coupled to the Azure Blob SDK — can't run/test without Azure
var blob = _container.GetBlobClient($"{Guid.NewGuid()}{ext}");
await blob.UploadAsync(file.OpenReadStream(), cancellationToken: ct);

A minimal IFileStorage interface — SaveAsync, GetAsync, ExistsAsync, DeleteAsync. Domain code depends on the abstraction; the physical backend is chosen once in Program.cs. Ships with LocalDiskFileStorage for development; swap to Azure or S3 with one line — zero changes to domain code.

public partial class UploadPhotoAction : DomainAction<Uri>
{
private IFileStorage _storage = null!; // injected
public required IFormFile File { get; init; }
public override async Task<Result<Uri, IError>> Execute(CancellationToken ct)
{
await using var stream = File.OpenReadStream();
Uri uri = await _storage.SaveAsync(stream, File.FileName, container: "photos", ct);
return uri;
}
}
  • IFileStorage — four methods: SaveAsync (returns the file Uri), GetAsync (read stream, null if missing), ExistsAsync, DeleteAsync (idempotent — no-op if already gone).
  • Result-based contractSaveAsResultAsync / GetAsResultAsync / ExistsAsResultAsync / DeleteAsResultAsync return Result<T, IError> (VoidResult<IError> for delete) with typed errors — FileTooLargeError (413), StorageFileNotFoundError (404), StorageWriteError (500). Composes into an action/mutation with no try/catch. The throwing surface stays for direct use.
  • File metadataIFileInfoProvider.GetInfoAsync(uri) returns a StoredFileInfo (size, content type, last-modified) without downloading the file. Implemented by every provider.
  • Signed download URLsISignedUrlProvider.GetDownloadUrlAsync(uri, expiry) mints a temporary pre-authenticated URL so a private file is served straight from the backend, no proxying. Azure (SAS), S3/R2 (pre-signed), and Google Cloud (signed URL) implement it.
  • LocalDiskFileStorage — saves under {basePath}/files/{container}/ with GUID file names and relative URIs; optional maxFileSizeBytes upload cap (enforced for seekable and non-seekable streams); atomic writes (temp file + rename); path-traversal protection on both write (container) and read/delete (URI).
  • Cloud & network providers — Azure Blob (AzureBlobFileStorage), S3 / Cloudflare R2 / MinIO / Wasabi / Backblaze B2 (S3FileStorage), Google Cloud Storage (GoogleCloudFileStorage), SFTP (SftpFileStorage), and FTP / FTPS (FtpFileStorage) — each with size limits, URI/key validation, and idempotent delete.
  • InMemoryFileStorage — the Pragmatic.Storage.InMemory package for tests and local development; AddInMemoryStorage(), no filesystem or network.
  • MimeTypes — content-type from file extension (common web/office types, fallback application/octet-stream). Extension-based only — no content sniffing.
  • LimitedReadStream — read-only wrapper that enforces a byte cap on non-seekable streams (used by the S3 provider; reusable in custom providers).
  • RegistrationAddLocalDiskStorage(basePath[, maxFileSizeBytes]), AddInMemoryStorage(), AddAzureBlobStorage, AddS3Storage, AddGoogleCloudStorage, AddSftpStorage, AddFtpStorage, and AddFileStorage<T>() on IServiceCollection; UseStorage(factory) / UseStorage<T>() on IPragmaticBuilder.
  • Observability — providers log via [LoggerMessage] (zero-allocation structured logging).
Terminal window
dotnet add package Pragmatic.Storage
dotnet add package Pragmatic.Storage.Azure # or .S3 / .GoogleCloud / .Sftp / .Ftp / .InMemory
PackagePurpose
Pragmatic.StorageThe IFileStorage contract, LocalDiskFileStorage, Result-based surface and typed errors, IFileInfoProvider / ISignedUrlProvider, DI/builder extensions, MimeTypes, LimitedReadStream
Pragmatic.Storage.AzureAzure Blob Storage provider (AzureBlobFileStorage + AzureBlobStorageOptions) — metadata + SAS signed URLs
Pragmatic.Storage.S3Amazon S3 / Cloudflare R2 / MinIO / Wasabi / Backblaze B2 provider (S3FileStorage + S3StorageOptions) — metadata + pre-signed URLs
Pragmatic.Storage.GoogleCloudGoogle Cloud Storage provider (GoogleCloudFileStorage + GoogleCloudStorageOptions) — metadata + signed URLs
Pragmatic.Storage.SftpSFTP provider (SftpFileStorage + SftpStorageOptions) — metadata; no signed URLs
Pragmatic.Storage.FtpFTP / FTPS provider (FtpFileStorage + FtpStorageOptions) — metadata; no signed URLs
Pragmatic.Storage.InMemoryInMemoryFileStorage for tests and local development (AddInMemoryStorage())

Register the backend in Program.cs (app.UseStorage(...)) — LocalDisk for dev, Azure/S3 in production. Domain code is unchanged across environments.

Stable within 1.0.0-alpha — the IFileStorage contract, LocalDiskFileStorage, and the Azure/S3 providers are settled. See the roadmap.

| Concepts | The abstraction, container organization, entity file-reference pattern | | Getting Started | Save/get/delete a file, wire a backend, environment switching | | Custom Providers | Implement IFileStorage for a new backend | | Common Mistakes | The most frequent storage pitfalls | | Troubleshooting | Problem/solution guide |

  • .NET 10.0+

Part of the Pragmatic.Design ecosystem — see Licensing. Pragmatic.Storage is MIT-licensed.