StorageConnector 10.1.0
dotnet add package StorageConnector --version 10.1.0
NuGet\Install-Package StorageConnector -Version 10.1.0
<PackageReference Include="StorageConnector" Version="10.1.0" />
<PackageVersion Include="StorageConnector" Version="10.1.0" />
<PackageReference Include="StorageConnector" />
paket add StorageConnector --version 10.1.0
#r "nuget: StorageConnector, 10.1.0"
#:package StorageConnector@10.1.0
#addin nuget:?package=StorageConnector&version=10.1.0
#tool nuget:?package=StorageConnector&version=10.1.0
StorageConnector
Let your users upload files straight to cloud storage â without the bytes ever passing through your server. đ
StorageConnector is an open-source C# library that generates short-lived, pre-signed upload and download URLs for Azure Blob Storage, AWS S3 and Google Cloud Storage, behind a single friendly API. Your app hands the URL to the browser or mobile client, and the client sends the file straight to the cloud.
Every one of those clouds already supports this. Each of them does it differently. StorageConnector gives you one way to ask.
⥠How it works
Browser / mobile app Your API Cloud storage
â â â
â 1. "I want to upload" â â
â ââââââââââââââââââââââââââē â â
â â 2. GenerateDirectUploadInfo(âĻ)â
â 3. URL + method + headers â â
â ââââââââââââââââââââââââââ â â
â â
â 4. PUT the file bytes directly to the signed URL â
â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââē â
Your server never sees the file â so no upload bandwidth through your app, no memory pressure from buffering, and no request timeouts on large files. đ
⨠Features
- đ Pre-Signed URLs - Short-lived, secure upload and download URLs for direct client-side transfers
- đ Cloud Portability - Write your upload logic once and switch clouds with a config change. One provider is active per deployment â this is about moving between clouds, not using several at the same time
- đ Country-Based Account Routing - Choose which storage account or bucket a file lands in, based on the user's ISO country code
- đ¤ Face Recognition - Built for selfie and identity-check flows: count faces in an uploaded image, match against people you registered earlier, and erase someone's biometric data on request
- đ Dependency Injection - First-class support for ASP.NET Core DI
- đ¯ Type Safety - Strongly-typed configuration and DTOs, with
CancellationTokenthroughout - đĻ Easy Configuration - Simple JSON-based configuration
đ§ What it doesn't do
StorageConnector is a pre-signed URL generator, not a general-purpose storage abstraction. There's no API for listing, deleting, copying or moving objects, reading or writing metadata, or streaming file content through your server. Need those? Reach for your cloud provider's own SDK â it works happily alongside this library on the same account. đ
đ Licence heads-up: StorageConnector is GPLv3. That's a strong copyleft licence and it affects what you can build on top of it, so do check it suits your project before adopting â see License.
đĻ Installation
NuGet Package
Install via .NET CLI:
dotnet add package StorageConnector
Or via Package Manager Console:
Install-Package StorageConnector
Package Links:
- đĻ NuGet Gallery
- đģ GitHub Repository
đ Quick Start
1. Configure Services
Add StorageConnector to your Program.cs or Startup.cs:
builder.Services.AddStorageConnectors();
Configuration is read from the StorageConnectors section of the application's IConfiguration, so
no argument is needed. The call returns the IServiceCollection, so it chains.
2. Configuration
Add the following to your appsettings.json:
â ī¸ Security Warning: Never commit real credentials to source control. Use environment variables, Azure Key Vault, AWS Secrets Manager, or similar secure storage for production credentials.
StorageConnector currently authenticates with long-lived keys only (Azure account keys, AWS access keys, a GCP service account key). It does not yet support Managed Identity, IAM roles for service accounts, or Workload Identity Federation â any of which would remove these secrets from configuration entirely. If you are deploying into Azure, EKS or GKE, treat that as a known gap.
âšī¸ Country codes: keys in
CountryIsoCodeMapToAccountNamemust be ISO 3166-1 alpha-2 country codes (DK,DE,US,IN). Region groupings such asEUare not country codes and will fail at start-up.
{
"StorageConnectors": {
"Azure": {
"CountryIsoCodeMapToAccountName": {
"US": "yourstorageaccount"
},
"Accounts": [
{
"AccountName": "yourstorageaccount",
"AccountKey": "YOUR_AZURE_STORAGE_ACCOUNT_KEY",
"ContainerName": "your-container-name"
}
]
},
"AWS": {
"AwsCredentials": {
"AccessKey": "YOUR_AWS_ACCESS_KEY",
"SecretAccessKey": "YOUR_AWS_SECRET_KEY"
},
"CountryIsoCodeMapToAccountName": {
"DE": "your-s3-bucket"
},
"Accounts": [
{
"BucketName": "your-s3-bucket",
"AwsRegion": "eu-west-1",
"AwsCredentials": {
"AccessKey": "YOUR_AWS_ACCESS_KEY",
"SecretAccessKey": "YOUR_AWS_SECRET_KEY"
}
}
]
},
"GCP": {
"GcpCredentials": {
"type": "service_account",
"project_id": "your-project-id",
"private_key_id": "YOUR_PRIVATE_KEY_ID",
"private_key": "-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY\n-----END PRIVATE KEY-----\n",
"client_email": "your-service-account@your-project.iam.gserviceaccount.com",
"client_id": "YOUR_CLIENT_ID",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/your-service-account%40your-project.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
},
"CountryIsoCodeMapToAccountName": {
"IN": "your-gcp-bucket"
},
"Accounts": [
{
"BucketName": "your-gcp-bucket",
"ServiceAccountEmail": "your-service-account@your-project.iam.gserviceaccount.com"
}
]
}
}
}
3. Usage Example
Inject StorageConnectorService into your classes:
using StorageConnector;
using EarthCountriesInfo;
public class FileUploadController : ControllerBase
{
private readonly StorageConnectorService _storageConnectorService;
public FileUploadController(StorageConnectorService storageConnectorService)
{
_storageConnectorService = storageConnectorService;
}
[HttpPost("generate-upload-url")]
public async Task<IActionResult> GenerateUploadUrl(
[FromBody] UploadRequest request,
CancellationToken cancellationToken)
{
// Generate a pre-signed upload URL for client-side upload
var uploadInfo = await _storageConnectorService.GenerateDirectUploadInfo(
countryOfResidenceIsoCode: CountryIsoCode.US,
fileReferenceWithPath: new CloudFileName($"uploads/{Guid.NewGuid()}"),
contentType: "image/png",
expiryInMinutes: 15,
cancellationToken: cancellationToken
);
// uploadInfo.FileName is the key the object is stored under - save it, you will
// need it to generate a download URL later.
return Ok(uploadInfo);
}
}
Response Model:
public sealed record UploadInfo
{
// The object key the file is actually stored under. The service appends the extension
// derived from the content type, so this may differ from the name you requested.
// Persist it - it is the handle you pass to GenerateDirectDownloadInfo later.
[JsonPropertyName("fileName")]
public required CloudFileName FileName { get; init; }
[JsonPropertyName("directUploadUrl")]
public required string DirectUploadUrl { get; init; }
[JsonPropertyName("method")]
public required string HttpMethod { get; init; }
[JsonPropertyName("headers")]
public required Dictionary<string, string> Headers { get; init; }
}
â ī¸ Upgrading from 1.x:
UploadInfo.FileNameis new. Requesting an upload forphotowith content typeimage/jpegstores the object asphoto.jpg, so saveFileNamealongside your own records â without it the download URL you generate later will not resolve.
4. Upload from the client
UploadInfo tells the client everything it needs: where to send the file, which HTTP method to use,
and which headers to set. Send the bytes exactly as described â the headers are part of what was
signed, so changing them will make the cloud reject the request.
// uploadInfo is the JSON your endpoint returned
const response = await fetch(uploadInfo.directUploadUrl, {
method: uploadInfo.method, // "PUT"
headers: uploadInfo.headers, // e.g. { "Content-Type": "image/png" }
body: file, // the File / Blob straight from an <input type="file">
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
// Send uploadInfo.fileName back to your API and store it - it is how you
// reference this object from now on.
The file goes from the user's device to the cloud. It never touches your API.
đ Key Concepts
Country-Based Account Routing
StorageConnector routes files to different accounts or buckets within a single configured cloud provider, based on the user's country ISO code:
// A German user's file goes to whichever account "DE" maps to in your configuration
var uploadInfo = await _storageConnectorService.GenerateDirectUploadInfo(
CountryIsoCode.DE, // Germany
new CloudFileName("user-data/profile.jpg"),
"image/jpeg",
cancellationToken: cancellationToken
);
â ī¸ Scope of this feature. One cloud provider is active per deployment â if AWS is configured, Azure and GCP are not consulted. Routing selects an account within that provider; it does not route between clouds.
Placing data in a given region can be one input to a data-residency strategy, but this library does not by itself make a deployment GDPR-compliant. Where the bucket physically lives, what your cloud provider's terms say, sub-processor arrangements, and transfer mechanisms are all outside its control. Keys in
CountryIsoCodeMapToAccountNameare ISO 3166-1 alpha-2 country codes (DK,DE,US) â region groupings such asEUare not valid and will fail at start-up.
Downloading
Downloads work the same way in reverse: you generate a short-lived URL and the client fetches the file straight from the cloud.
var downloadInfo = await _storageConnectorService.GenerateDirectDownloadInfo(
countryOfResidenceIsoCode: CountryIsoCode.DK,
fileReferenceWithPath: storedFileName, // the UploadInfo.FileName you saved earlier
expiryInMinutes: 15,
cancellationToken: cancellationToken);
return Ok(new { url = downloadInfo.DirectDownloadUrl });
Pass the FileName returned by the upload, not the name you originally requested â the service
appends an extension derived from the content type, so the two can differ.
Provider support: download URLs are currently implemented for AWS only. Azure and GCP throw
NotSupportedException. Uploads work on all three.
URL lifetimes
expiryInMinutes defaults to 60 and controls how long a generated URL stays valid. Keep it as short
as your client flow allows â anyone holding the URL can use it until it expires.
Signed URLs are backdated by five minutes so a client whose clock runs slightly behind your server is not rejected. Google Cloud Storage caps a signed URL at seven days total, including that allowance.
Face Recognition
â ī¸ Biometric data. Face templates are special-category personal data under GDPR Article 9. Only
RegisterFaceAsyncstores anything; make sure you have a lawful basis before calling it, and wireDeleteRegisteredFacesAsyncinto your erasure process.
Reading and writing are separate calls, so nothing is stored unless you ask for it:
// READ - counts good-quality faces. Stores nothing.
int faces = await _storageConnectorService.CountFacesAsync(
regionCountryIsoCode: CountryIsoCode.DK,
fileNameWithExtension: new CloudFileName("faces/user123.jpg"),
cancellationToken);
// READ - who does this look like? Stores nothing, and does not add the queried face.
IReadOnlySet<string> matches = await _storageConnectorService.FindMatchingFacesAsync(
faceCollectionName: "user-faces",
regionCountryIsoCode: CountryIsoCode.DK,
fileNameWithExtension: new CloudFileName("faces/user123.jpg"),
cancellationToken);
// WRITE - stores a biometric template. Deliberate, and erasable.
RegisteredFace registered = await _storageConnectorService.RegisterFaceAsync(
faceCollectionName: "user-faces",
regionCountryIsoCode: CountryIsoCode.DK,
fileNameWithExtension: new CloudFileName("faces/user123.jpg"),
subjectId: "user-123",
cancellationToken);
// ERASE - honour a deletion request. Returns how many templates were removed.
int deleted = await _storageConnectorService.DeleteRegisteredFacesAsync(
faceCollectionName: "user-faces",
regionCountryIsoCode: CountryIsoCode.DK, // must match the country used to register
subjectId: "user-123",
cancellationToken);
â ī¸ Erasure and country codes. Templates live in the recognition service belonging to the account the country maps to. Pass the same
regionCountryIsoCodeyou registered with â erasing with a different country searches a different collection, finds nothing, and returns0as though the deletion succeeded. If you register faces, store the country alongside the subject id.
subjectId is your own identifier for the person and is restricted to letters, digits and _ . - :
(max 255 characters) â the character set AWS Rekognition permits. Use a user id or a hashed
reference rather than an email address or a display name.
Provider support:
| Operation | AWS | Azure | GCP |
|---|---|---|---|
CountFacesAsync |
â | â | â |
FindMatchingFacesAsync |
â | â | â |
RegisterFaceAsync |
â | â | â |
DeleteRegisteredFacesAsync |
â | â | â |
Azure requires a VisionAccount section (see configuration above). Check
SupportsFaceRecognition before calling, or handle NotSupportedException.
Detection thresholds are configurable under StorageConnectors:AWS:FaceQuality
(MinSharpness, MinBrightness, MinConfidence, MatchThreshold).
đī¸ Architecture
Inject StorageConnectorService and you can ignore everything below it â it picks the configured
provider and forwards the call.
It implements two interfaces, deliberately kept apart:
| Interface | Purpose |
|---|---|
IStorageProvider |
Pre-signed upload and download URLs |
IFaceRecognitionProvider |
Face counting, matching, registration and erasure |
They are separate because storing a biometric template is a very different act from generating an
upload URL, and bundling them once meant a read-shaped call was quietly writing special-category
personal data. GCPStorageService implements only the first â it has no face support at all, rather
than stubs that throw.
Behind the facade, one provider is active per deployment:
AmazonS3BucketServiceâ AWS S3 and RekognitionAzureBlobStorageServiceâ Azure Blob Storage and Face APIGCPStorageServiceâ Google Cloud Storage (V4 signed URLs, signed via IAM so no private key is held)
Selection order is AWS â Azure â GCP: the first with usable credentials wins. Within that provider, the country map chooses the account.
đ¤ Contributing
We welcome contributions! Here's how you can help:
- đ Report bugs - Open an issue
- đĄ Suggest features - Start a discussion
- đ§ Submit PRs - Fork, create a feature branch, and submit a pull request
Development Setup
git clone https://github.com/prmeyn/StorageConnector.git
cd StorageConnector
dotnet restore
dotnet build
đ Requirements
- .NET 10.0 or later
- Cloud provider accounts (Azure, AWS, and/or GCP)
đ License
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
đ Links
- đĻ NuGet Package
- đģ GitHub Repository
- đ Documentation
- đ Issue Tracker
đ Acknowledgments
Built with â¤ī¸ using:
Happy coding! đđđ
| 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
- AWSSDK.Rekognition (>= 4.0.100.7)
- AWSSDK.S3 (>= 4.0.101.6)
- Azure.AI.Vision.Face (>= 1.0.0-beta.2)
- Azure.Storage.Blobs (>= 12.29.1)
- EarthCountriesInfo (>= 10.2.0)
- Google.Cloud.Iam.Credentials.V1 (>= 2.5.0)
- Google.Cloud.Iam.V1 (>= 3.5.0)
- Microsoft.AspNetCore.StaticFiles (>= 2.3.11)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
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 |
|---|---|---|
| 10.1.0 | 0 | 7/31/2026 |
| 10.0.0 | 233 | 11/26/2025 |
| 6.1.0 | 341 | 4/5/2025 |
| 6.0.3 | 271 | 2/21/2025 |
| 6.0.2 | 177 | 2/20/2025 |
| 6.0.1 | 174 | 2/19/2025 |
| 6.0.0 | 194 | 2/14/2025 |
| 5.0.4 | 182 | 2/12/2025 |
| 5.0.3 | 182 | 2/12/2025 |
| 5.0.2 | 169 | 2/11/2025 |
| 5.0.1 | 184 | 2/11/2025 |
| 5.0.0 | 186 | 2/10/2025 |
| 4.0.3 | 173 | 2/9/2025 |
| 4.0.2 | 191 | 2/9/2025 |
| 4.0.1 | 169 | 2/9/2025 |
| 4.0.0 | 186 | 2/9/2025 |
| 3.0.0 | 230 | 2/2/2025 |
| 2.0.0 | 202 | 1/29/2025 |
| 1.2.0 | 218 | 1/29/2025 |
| 1.1.0 | 211 | 1/26/2025 |
2.0.0 - breaking release. Fixes several start-up crashes (missing optional configuration sections no
longer throw), corrects pre-signed upload URLs so the resolved object key is returned on
UploadInfo.FileName, replaces the content-type to extension lookup (HEIC, CSV, XML and ZIP uploads
now work; JPEG resolves to .jpg rather than .jpe), and separates face recognition into
IFaceRecognitionProvider where storing a biometric template is an explicit call with a matching
erasure API. HasAccounts is now a synchronous property, every async method accepts a
CancellationToken, and Google Cloud Storage URLs use V4 signing. See the README for upgrade notes.