Storage.Vector
1.1.0
dotnet add package Storage.Vector --version 1.1.0
NuGet\Install-Package Storage.Vector -Version 1.1.0
<PackageReference Include="Storage.Vector" Version="1.1.0" />
<PackageVersion Include="Storage.Vector" Version="1.1.0" />
<PackageReference Include="Storage.Vector" />
paket add Storage.Vector --version 1.1.0
#r "nuget: Storage.Vector, 1.1.0"
#:package Storage.Vector@1.1.0
#addin nuget:?package=Storage.Vector&version=1.1.0
#tool nuget:?package=Storage.Vector&version=1.1.0
Storage.Vector
A portable .NET 8 storage provider abstraction and implementations for Azure Blob Storage (including local Azurite emulation) and Local Filesystem (supporting local directories, NAS, and SMB/NFS mounts). Supports secondary/backup storage mirroring, presigned download URLs, path traversal protection, and validation.
Features
- 📁 Unified interface — swap between local directories, NAS, and cloud providers purely via configuration
- ☁️ Azurite & Azure support — fully compatible with local Azurite emulator for dev/testing and cloud Azure Blob Storage
- 🔒 Path traversal protection — local provider containment checks prevent directory breakout attacks
- 👯 Keyed secondary mirroring — configure and inject independent backup/sync storage targets via keyed DI
- 🔑 Presigned URLs — generate signed download URLs (HMAC-SHA256 signatures for LocalFile, SAS tokens for Azure)
- ☁️ AWS S3 support — first-class S3 provider with single-bucket mode (
container= key prefix), presigned URL generation, and LocalStack / MinIO compatibility viaAwsServiceUrl - 🚀 High Performance & Zero-Allocation — optimized via thread-safe in-memory metadata caches, pre-computed path normalizations, and span-based zero-allocation url signing
- ⚙️ Startup validation — throws clear errors on application boot if options or paths are missing
- 📦 NuGet-ready — structured for
dotnet packwith symbols (.snupkg) - 💉 DI-friendly — integrates with
Microsoft.Extensions.DependencyInjectionviaAddStorageProvider() - 🛡️ Unified error handling — catches and translates underlying API exceptions into a structured
StorageException
Quick Start
Install
dotnet add package Storage.Vector
Register with Dependency Injection
To register the primary storage provider:
// Program.cs / Startup.cs
builder.Services.AddStorageProvider(builder.Configuration);
Configure the provider options in your settings:
// appsettings.json
{
"Storage": {
"Provider": "LocalFile", // "LocalFile" or "AzureBlob"
"Container": "uploads",
"Local": {
"RootPath": "C:\\ProgramData\\MyApp\\Storage",
"PublicBaseUrl": "https://localhost:5001/api/v1/storage",
"SigningKey": "your-hmac-sha256-signing-key-minimum-32-chars-long"
}
}
}
AWS S3
{
"Storage": {
"Provider": "S3",
"Container": "my-app-bucket",
"AwsRegion": "eu-west-2",
"AwsAccessKeyId": "AKIAIOSFODNN7EXAMPLE",
"AwsSecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}
}
LocalStack / MinIO: Add
"AwsServiceUrl": "http://localhost:4566"and"AwsForcePathStyle": trueto target a local S3-compatible endpoint. OmitAwsAccessKeyId/AwsSecretAccessKeyentirely to use the ambient IAM credential chain (ECS task role, EC2 instance profile, etc.).
Object keys are namespaced as {container}/{key} within the configured S3 bucket, so a single bucket can serve multiple logical containers.
Upload and Download Files
Inject IStorageProvider into your services:
public class DocumentService(IStorageProvider storage)
{
public async Task SaveFileAsync(string key, Stream data, CancellationToken ct)
{
await storage.PutObjectAsync("documents", key, data, "application/pdf", ct);
}
public async Task<Stream> ReadFileAsync(string key, CancellationToken ct)
{
return await storage.GetObjectAsync("documents", key, ct);
}
}
Fluent Scoped Operations
To avoid repeating the container name or key in consecutive actions, you can scope your operations using the fluent API:
public class DocumentService(IStorageProvider storage)
{
public async Task ProcessInvoiceAsync(string key, Stream data, CancellationToken ct)
{
// 1. Scope to a container
var container = storage.Container("documents");
await container.EnsureExistsAsync(ct);
// 2. Scope to a file
var file = container.File(key);
// 3. Perform actions on that file fluently
await file.UploadAsync(data, "application/pdf", ct);
var downloadUrl = await file.GetPresignedUrlAsync(TimeSpan.FromMinutes(15), ct);
using var downloadStream = await file.DownloadAsync(ct);
}
}
Register a Keyed Secondary Provider (Backup Sync)
// Register Primary IStorageProvider
builder.Services.AddStorageProvider(builder.Configuration);
// Register Secondary Keyed IStorageProvider ("secondary")
builder.Services.AddSecondaryStorageProvider(builder.Configuration);
Configure both in settings:
{
"Storage": {
"Provider": "LocalFile",
"Container": "uploads",
"Local": {
"RootPath": "C:\\Storage\\Primary",
"PublicBaseUrl": "https://localhost:5001/storage",
"SigningKey": "primary-key"
},
"Secondary": {
"Provider": "AzureBlob",
"Container": "backups",
"Azure": {
"ConnectionString": "UseDevelopmentStorage=true"
}
}
}
}
Resolve the secondary provider using the SecondaryProviderKey constant:
public class SyncService(
IStorageProvider primary,
[FromKeyedServices(StorageServiceCollectionExtensions.SecondaryProviderKey)] IStorageProvider secondary)
{
public async Task MirrorAsync(string key, CancellationToken ct)
{
using var data = await primary.GetObjectAsync("documents", key, ct);
await secondary.PutObjectAsync("documents", key, data, "application/octet-stream", ct);
}
}
Without DI (direct use)
// LocalFile
var localOptions = Options.Create(new StorageOptions
{
Provider = "LocalFile",
RootPath = "C:\\Storage",
PublicBaseUrl = "https://localhost:5001/storage",
SigningKey = "secret-signing-key"
});
IStorageProvider localProvider = new LocalFileStorageProvider(localOptions);
// AzureBlob
var azureOptions = Options.Create(new StorageOptions
{
Provider = "AzureBlob",
Container = "media",
ConnectionString = "UseDevelopmentStorage=true"
});
var client = new BlobServiceClient(azureOptions.Value.ConnectionString);
IStorageProvider azureProvider = new AzureBlobStorageProvider(client, azureOptions);
Detailed API & Options Reference
Generating and Validating Presigned URLs
Generate a URL that routes download requests through your local endpoint and validates them with a signature:
// Generate
var url = await storage.GetPresignedUrlAsync("documents", "invoice.pdf", TimeSpan.FromMinutes(15), ct);
// Validate (in your Controller/Endpoint)
var signer = new LocalFileUrlSigner(options.Value.SigningKey);
var requestUrl = $"{Request.Path}{Request.QueryString}";
if (!signer.VerifyUrl(requestUrl))
{
return Forbid("Presigned URL is expired or has an invalid signature.");
}
Unified Exception Handling
All underlying filesystem or Azure SDK network/authorization errors are mapped into a StorageException containing a StorageErrorKind enum:
try
{
await storage.GetObjectAsync("documents", "missing.pdf", ct);
}
catch (StorageException ex)
{
switch (ex.ErrorKind)
{
case StorageErrorKind.NotFound:
Console.WriteLine("File or container not found.");
break;
case StorageErrorKind.AccessDenied:
Console.WriteLine("Unauthorized access to storage path.");
break;
case StorageErrorKind.Transient:
Console.WriteLine("Transient network issue. Retry later.");
break;
default:
Console.WriteLine($"Storage operation failed: {ex.Message}");
break;
}
}
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
Storage:Provider |
string |
(None) | Storage engine selection: "LocalFile" or "AzureBlob" |
Storage:Container |
string |
(None) | Default container/folder name to build roots in |
Storage:Local:RootPath |
string |
(None) | Directory containing storage containers (LocalFile only) |
Storage:Local:PublicBaseUrl |
string |
(None) | Base URL to route signed requests (LocalFile only) |
Storage:Local:SigningKey |
string |
(None) | Secret key used to sign URLs (LocalFile only) |
Storage:Azure:ConnectionString |
string |
(None) | Storage Account Connection String (AzureBlob only) |
Storage:Azure:PublicBlobEndpoint |
string |
(None) | Optional CDN public blob endpoint overlay (AzureBlob only) |
License
This project is licensed under the PolyForm Noncommercial License 1.0.0.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
-
net8.0
- AWSSDK.S3 (>= 4.0.101.4)
- Azure.Storage.Blobs (>= 12.29.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.