SW.CloudFiles is a unified, multi-cloud storage abstraction library for .NET 8 that provides a consistent interface across different cloud storage providers. It simplifies cloud storage operations by offering a single API that works with multiple cloud providers without vendor lock-in.
- 🚀 S3-Compatible Storage (AWS S3, DigitalOcean Spaces, MinIO, etc.)
- ☁️ Azure Blob Storage
- 🌐 Google Cloud Storage
- 🔶 Oracle Cloud Storage
- 🧪 Local Filesystem — for testing and local development only
The library provides both core functionality and ASP.NET Core dependency injection extensions for easy integration into web applications.
Choose the appropriate package based on your cloud storage provider:
dotnet add package SimplyWorks.CloudFiles.S3.Extensionsdotnet add package SimplyWorks.CloudFiles.AS.Extensionsdotnet add package SimplyWorks.CloudFiles.GC.Extensionsdotnet add package SimplyWorks.CloudFiles.OC.Extensionsdotnet add package SimplyWorks.CloudFiles.LocalTests.ExtensionsInstall the core package (e.g.
SimplyWorks.CloudFiles.S3) only if you need to reference the service or options types directly without the DI extensions.
All providers implement the same ICloudFilesService interface from SimplyWorks.PrimitiveTypes, ensuring a consistent API.
{
"CloudFiles": {
"AccessKeyId": "your-access-key",
"SecretAccessKey": "your-secret-key",
"BucketName": "your-bucket-name",
"ServiceUrl": "https://your-service-url"
}
}// S3-Compatible Storageservices.AddS3CloudFiles(o =>{o.AccessKeyId="your-access-key";o.SecretAccessKey="your-secret-key";o.ServiceUrl="https://s3.amazonaws.com";o.BucketName="your-bucket-name";});// Azure Blob Storageservices.AddAsCloudFiles(o =>{o.AccessKeyId="your-account-name";o.SecretAccessKey="your-account-key";o.ServiceUrl="https://youraccount.blob.core.windows.net";o.BucketName="your-container-name";});// Google Cloud Storageservices.AddGoogleCloudFiles(o =>{o.ProjectId="your-project-id";o.BucketName="your-bucket-name";o.ClientEmail="service-account@project.iam.gserviceaccount.com";o.PrivateKey="-----BEGIN RSA PRIVATE KEY-----\n...";// ... other service account fields});// Oracle Cloud Storageservices.AddOracleCloudFiles(o =>{o.TenantId="your-tenant-ocid";o.UserId="your-user-ocid";o.FingerPrint="xx:xx:xx:...";o.Region="us-ashburn-1";o.BucketName="your-bucket-name";o.NamespaceName="your-namespace";o.RSAKey="-----BEGIN RSA PRIVATE KEY-----\n...";});// Local Filesystem — testing and local development onlyservices.AddLocalTestsCloudFiles(o =>{o.BucketName="my-test-bucket";// StoragePath is optional — defaults to {GetTempPath()}/SW.CloudFiles.LocalTests/{BucketName}});Inject ICloudFilesService into your controllers or services:
publicclassFileController:ControllerBase{privatereadonlyICloudFilesService_cloudFilesService;publicFileController(ICloudFilesServicecloudFilesService){_cloudFilesService=cloudFilesService;}}varresult=await_cloudFilesService.WriteAsync(fileStream,newWriteFileSettings{Key="uploads/document.pdf",ContentType="application/pdf",Public=true,Metadata=newDictionary<string,string>{["UploadedBy"]="user123"}});// result.Location contains the public URL or a 1-hour signed URLvarresult=await_cloudFilesService.WriteTextAsync("Hello, World!",newWriteFileSettings{Key="messages/hello.txt",ContentType="text/plain"});usingvarstream=await_cloudFilesService.OpenReadAsync("uploads/document.pdf");usingvarfileStream=File.Create("local-copy.pdf");awaitstream.CopyToAsync(fileStream);varfiles=await_cloudFilesService.ListAsync("uploads/");foreach(varfileinfiles)Console.WriteLine($"{file.Key}{file.Size} bytes");// Permanent public URLvarpublicUrl=_cloudFilesService.GetUrl(key);// Time-limited signed URLvarsignedUrl=_cloudFilesService.GetSignedUrl(key,TimeSpan.FromHours(2));booldeleted=await_cloudFilesService.DeleteAsync("uploads/document.pdf");varmetadata=await_cloudFilesService.GetMetadataAsync(key);// Always includes: ContentType, Hash, ContentLength[ApiController][Route("api/[controller]")]publicclassFilesController:ControllerBase{privatereadonlyICloudFilesService_cloudFilesService;publicFilesController(ICloudFilesServicecloudFilesService){_cloudFilesService=cloudFilesService;}[HttpPost("upload")]publicasyncTask<IActionResult>UploadFile([FromForm]IFormFilefile){if(file==null||file.Length==0)returnBadRequest("No file uploaded");varresult=await_cloudFilesService.WriteAsync(file.OpenReadStream(),newWriteFileSettings{Key=$"uploads/{Guid.NewGuid()}/{file.FileName}",ContentType=file.ContentType,Public=true,CloseInputStream=false});returnOk(new{url=result.Location,size=result.Size,fileName=result.Name});}[HttpGet("download/{*filePath}")]publicasyncTask<IActionResult>DownloadFile(stringfilePath){try{varstream=await_cloudFilesService.OpenReadAsync(filePath);varmetadata=await_cloudFilesService.GetMetadataAsync(filePath);returnFile(stream,metadata["ContentType"],Path.GetFileName(filePath));}catch{returnNotFound();}}[HttpDelete("{*filePath}")]publicasyncTask<IActionResult>DeleteFile(stringfilePath){varsuccess=await_cloudFilesService.DeleteAsync(filePath);returnsuccess?Ok():NotFound();}}Three of the four providers automatically create delete lifecycle rules for temp-prefix objects on startup. This is useful for objects that are only needed temporarily.
| Prefix | Expiry |
|---|---|
temp1/ | 1 day |
temp7/ | 7 days |
temp30/ | 30 days |
temp365/ | 365 days |
Rules are only added if they don't already exist — existing rules are never modified or removed.
Set DisableAutoLifecycle = true in any provider's options to skip the automatic rule setup:
services.AddS3CloudFiles(o =>{// ... other configo.DisableAutoLifecycle=true;});services.AddGoogleCloudFiles(o =>{// ... other configo.DisableAutoLifecycle=true;});services.AddOracleCloudFiles(o =>{// ... other configo.DisableAutoLifecycle=true;});| Provider | Lifecycle support | Notes |
|---|---|---|
| S3 | ✅ Automatic | Rules applied via S3 Lifecycle Configuration API |
| Google Cloud | ✅ Automatic | Rules applied via GCS Bucket Lifecycle API; bucket is created if it doesn't exist |
| Oracle Cloud | ✅ Automatic | Rules applied via OCI Object Lifecycle Policy API |
| Azure Blob | Azure lifecycle management requires the Azure Resource Manager (ARM) plane, which is separate from the data-plane SDK used by this library. Configure lifecycle rules via the Azure Portal, Azure CLI (az storage account management-policy create), or ARM templates. |
All four providers support GetSignedUrl(key, expiry):
| Provider | Mechanism |
|---|---|
| S3 | Pre-signed URL (AWS Signature V4) |
| Google Cloud | Signed URL (V4 signing via service account credentials) |
| Azure (shared key) | Blob SAS token |
| Azure (managed identity) | User Delegation SAS token |
| Oracle Cloud | Pre-Authenticated Request (PAR) — creates a server-side resource on Oracle |
Note: Oracle PARs are server-side objects. Each call to
GetSignedUrlcreates a new PAR on Oracle Cloud.
- Works with AWS S3, DigitalOcean Spaces, MinIO, and any S3-compatible service.
- Bucket is created automatically if it does not exist.
- Container is created automatically if it does not exist.
- Managed Identity: Set
Managed = true. Optionally setManagedIdentityClientIdfor a user-assigned identity; omit it to use the system-assigned identity or ambientDefaultAzureCredential. - Public URL override: If you connect via private link (
ServiceUrl) but need public-facing URLs, setPublicServiceUrlto the standard public endpoint (e.g.https://account.blob.core.windows.net).
// Managed Identity exampleservices.AddAsCloudFiles(o =>{o.ServiceUrl="https://youraccount.blob.core.windows.net";o.BucketName="your-container";o.Managed=true;o.ManagedIdentityClientId="your-client-id";// omit for system-assignedo.PublicServiceUrl="https://youraccount.blob.core.windows.net";// optional});- Requires a service account with Storage Object Admin role on the bucket.
- Bucket is created automatically if it does not exist (requires Storage Admin role on the project).
GetSignedUrluses V4 signing via the service account's private key.
- OCI credentials (
UserId,TenantId,FingerPrint,RSAKey) are written to temporary files on startup and used to authenticate viaConfigFileAuthenticationDetailsProvider. GetSignedUrlcreates a Pre-Authenticated Request (PAR) with read-only access.
⚠️ Do not use this provider in production. It is designed exclusively for unit and integration tests and local development workflows.
Files are stored on the local filesystem. The storage root is chosen using .NET's Path.GetTempPath() — so the correct OS temp directory is used automatically (/tmp on Linux/macOS, %TEMP% on Windows). The final path is {GetTempPath()}/SW.CloudFiles.LocalTests/{BucketName}.
File cleanup is the caller's responsibility. On Linux and macOS the OS typically clears temp files on reboot, but on Windows temp files persist indefinitely. Always call Cleanup() in test teardown:
// Startup / DI registration (e.g. in TestStartup.cs)services.AddLocalTestsCloudFiles(o =>{o.BucketName="my-test-bucket";// StoragePath is optional — you almost never need to set it});// MSTest example — inject the concrete type for teardown[ClassInitialize]publicstaticvoidInit(TestContextctx){// build _serviceProvider ...}[ClassCleanup]publicstaticvoidCleanup(){_serviceProvider.GetRequiredService<CloudFilesService>().Cleanup();}GetUrl and GetSignedUrl both return a file:// URI. GetSignedUrl accepts the expiry parameter for interface compatibility but it has no effect.
OpenWrite throws NotImplementedException (same as Oracle and Azure).
Overriding the storage path — only needed for special scenarios such as Docker bind-mounts or CI artifact collection:
services.AddLocalTestsCloudFiles(o =>{o.BucketName="my-test-bucket";o.StoragePath="/mnt/ci-artifacts/cloudfiles";// absolute path, any OS});publicinterfaceICloudFilesService{Task<RemoteBlob>WriteAsync(StreaminputStream,WriteFileSettingssettings);Task<RemoteBlob>WriteTextAsync(stringtext,WriteFileSettingssettings);Task<Stream>OpenReadAsync(stringkey);Task<IEnumerable<CloudFileInfo>>ListAsync(stringprefix);Task<IReadOnlyDictionary<string,string>>GetMetadataAsync(stringkey);Task<bool>DeleteAsync(stringkey);stringGetUrl(stringkey);stringGetSignedUrl(stringkey,TimeSpanexpiry);WriteWrapperOpenWrite(WriteFileSettingssettings);}- .NET 8.0 or later
- Appropriate cloud provider account and credentials
- SimplyWorks.PrimitiveTypes
Contributions are welcome! Please read our Contributing Guidelines and Code of Conduct.
This project is licensed under the MIT License — see the LICENSE file for details.
SW.CloudFiles is developed and maintained by Simplify9.