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 Storage
services.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 Storage
services.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 Storage
services.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 Storage
services.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 only
services.AddLocalTestsCloudFiles(o => {
o.BucketName = "my-test-bucket";
// StoragePath is optional — defaults to {GetTempPath()}/SW.CloudFiles.LocalTests/{BucketName}
});Inject ICloudFilesService into your controllers or services:
public class FileController : ControllerBase
{
private readonly ICloudFilesService _cloudFilesService;
public FileController(ICloudFilesService cloudFilesService)
{
_cloudFilesService = cloudFilesService;
}
}var result = await _cloudFilesService.WriteAsync(fileStream, new WriteFileSettings
{
Key = "uploads/document.pdf",
ContentType = "application/pdf",
Public = true,
Metadata = new Dictionary<string, string>
{
["UploadedBy"] = "user123"
}
});
// result.Location contains the public URL or a 1-hour signed URLvar result = await _cloudFilesService.WriteTextAsync("Hello, World!", new WriteFileSettings
{
Key = "messages/hello.txt",
ContentType = "text/plain"
});using var stream = await _cloudFilesService.OpenReadAsync("uploads/document.pdf");
using var fileStream = File.Create("local-copy.pdf");
await stream.CopyToAsync(fileStream);var files = await _cloudFilesService.ListAsync("uploads/");
foreach (var file in files)
Console.WriteLine($"{file.Key} {file.Size} bytes");// Permanent public URL
var publicUrl = _cloudFilesService.GetUrl(key);
// Time-limited signed URL
var signedUrl = _cloudFilesService.GetSignedUrl(key, TimeSpan.FromHours(2));bool deleted = await _cloudFilesService.DeleteAsync("uploads/document.pdf");var metadata = await _cloudFilesService.GetMetadataAsync(key);
// Always includes: ContentType, Hash, ContentLength[ApiController]
[Route("api/[controller]")]
public class FilesController : ControllerBase
{
private readonly ICloudFilesService _cloudFilesService;
public FilesController(ICloudFilesService cloudFilesService)
{
_cloudFilesService = cloudFilesService;
}
[HttpPost("upload")]
public async Task<IActionResult> UploadFile([FromForm] IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest("No file uploaded");
var result = await _cloudFilesService.WriteAsync(file.OpenReadStream(), new WriteFileSettings
{
Key = $"uploads/{Guid.NewGuid()}/{file.FileName}",
ContentType = file.ContentType,
Public = true,
CloseInputStream = false
});
return Ok(new { url = result.Location, size = result.Size, fileName = result.Name });
}
[HttpGet("download/{*filePath}")]
public async Task<IActionResult> DownloadFile(string filePath)
{
try
{
var stream = await _cloudFilesService.OpenReadAsync(filePath);
var metadata = await _cloudFilesService.GetMetadataAsync(filePath);
return File(stream, metadata["ContentType"], Path.GetFileName(filePath));
}
catch
{
return NotFound();
}
}
[HttpDelete("{*filePath}")]
public async Task<IActionResult> DeleteFile(string filePath)
{
var success = await _cloudFilesService.DeleteAsync(filePath);
return success ? 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 config
o.DisableAutoLifecycle = true;
});
services.AddGoogleCloudFiles(o => {
// ... other config
o.DisableAutoLifecycle = true;
});
services.AddOracleCloudFiles(o => {
// ... other config
o.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 example
services.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-assigned
o.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]
public static void Init(TestContext ctx)
{
// build _serviceProvider ...
}
[ClassCleanup]
public static void Cleanup()
{
_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
});public interface ICloudFilesService
{
Task<RemoteBlob> WriteAsync(Stream inputStream, WriteFileSettings settings);
Task<RemoteBlob> WriteTextAsync(string text, WriteFileSettings settings);
Task<Stream> OpenReadAsync(string key);
Task<IEnumerable<CloudFileInfo>> ListAsync(string prefix);
Task<IReadOnlyDictionary<string, string>> GetMetadataAsync(string key);
Task<bool> DeleteAsync(string key);
string GetUrl(string key);
string GetSignedUrl(string key, TimeSpan expiry);
WriteWrapper OpenWrite(WriteFileSettings settings);
}- .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.