File Storage
File storage provides abstracted file operations with multiple backend implementations. Foundatio's IFileStorage interface allows you to work with files consistently across local disk, cloud storage, and more.
The IFileStorage Interface
public interface IFileStorage : IHaveSerializer, IDisposable
{
Task<Stream?> GetFileStreamAsync(string path, StreamMode streamMode,
CancellationToken cancellationToken = default);
Task<FileSpec?> GetFileInfoAsync(string path);
Task<bool> ExistsAsync(string path);
Task<bool> SaveFileAsync(string path, Stream stream,
CancellationToken cancellationToken = default);
Task<bool> RenameFileAsync(string path, string newPath,
CancellationToken cancellationToken = default);
Task<bool> CopyFileAsync(string path, string targetPath,
CancellationToken cancellationToken = default);
Task<bool> DeleteFileAsync(string path, CancellationToken cancellationToken = default);
Task<int> DeleteFilesAsync(string? searchPattern = null,
CancellationToken cancellation = default);
Task<PagedFileListResult> GetPagedFileListAsync(int pageSize = 100,
string? searchPattern = null,
CancellationToken cancellationToken = default);
}Implementations
InMemoryFileStorage
An in-memory storage for development and testing:
using Foundatio.Storage;
var storage = new InMemoryFileStorage();
// Save a file
await storage.SaveFileAsync("documents/report.pdf", pdfStream);
// Read a file
var stream = await storage.GetFileStreamAsync("documents/report.pdf", StreamMode.Read);FolderFileStorage
File storage backed by the local file system:
using Foundatio.Storage;
var storage = new FolderFileStorage(o => o.Folder = "/data/files");
// Files are stored in /data/files/documents/report.pdf
await storage.SaveFileAsync("documents/report.pdf", pdfStream);ScopedFileStorage
Prefix all paths with a scope:
using Foundatio.Storage;
var baseStorage = new FolderFileStorage(o => o.Folder = "/data");
var tenantStorage = new ScopedFileStorage(baseStorage, "tenant-abc");
// Path becomes: tenant-abc/documents/report.pdf
await tenantStorage.SaveFileAsync("documents/report.pdf", pdfStream);AzureFileStorage
Azure Blob Storage (separate package):
// dotnet add package Foundatio.AzureStorage
using Foundatio.AzureStorage.Storage;
var storage = new AzureFileStorage(o => {
o.ConnectionString = "DefaultEndpointsProtocol=https;...";
o.ContainerName = "files";
});S3FileStorage
AWS S3 Storage (separate package):
// dotnet add package Foundatio.AWS
using Foundatio.AWS.Storage;
var storage = new S3FileStorage(o => {
o.Region = RegionEndpoint.USEast1;
o.Bucket = "my-files";
});RedisFileStorage
Redis-backed storage (separate package):
// dotnet add package Foundatio.Redis
using Foundatio.Redis.Storage;
var storage = new RedisFileStorage(o => {
o.ConnectionMultiplexer = redis;
});MinioFileStorage
Minio object storage (separate package):
// dotnet add package Foundatio.Minio
using Foundatio.Minio.Storage;
var storage = new MinioFileStorage(o => {
o.Endpoint = "localhost:9000";
o.AccessKey = "minioadmin";
o.SecretKey = "minioadmin";
o.Bucket = "files";
});SshNetFileStorage
SFTP-backed storage (separate package):
// dotnet add package Foundatio.Storage.SshNet
using Foundatio.Storage.SshNet;
var storage = new SshNetFileStorage(o => {
o.Host = "sftp.example.com";
o.Username = "user";
o.Password = "password";
o.WorkingDirectory = "/uploads";
});Basic Operations
Saving Files
var storage = new InMemoryFileStorage();
// Save from stream
using var stream = File.OpenRead("local-file.pdf");
await storage.SaveFileAsync("remote/file.pdf", stream);
// Save string content (extension method)
await storage.SaveFileAsync("config.json", """{"key": "value"}""");
// Save with object serialization (extension method)
await storage.SaveObjectAsync("data/user.json", new User { Name = "John" });Reading Files
// Get file stream for reading
using var stream = await storage.GetFileStreamAsync("file.pdf", StreamMode.Read);
// Read as string (extension method)
string? content = await storage.GetFileContentsAsync("config.json");
// Read and deserialize (extension method)
var user = await storage.GetObjectAsync<User>("data/user.json");
// Get raw bytes (extension method)
byte[]? bytes = await storage.GetFileContentsRawAsync("image.png");File Information
// Check if file exists
bool exists = await storage.ExistsAsync("file.pdf");
// Get file info
var fileSpec = await storage.GetFileInfoAsync("file.pdf");
if (fileSpec != null)
{
Console.WriteLine($"Path: {fileSpec.Path}");
Console.WriteLine($"Size: {fileSpec.Size} bytes");
Console.WriteLine($"Modified: {fileSpec.Modified}");
Console.WriteLine($"Created: {fileSpec.Created}");
}Modifying Files
// Rename/move file
await storage.RenameFileAsync("old/path.pdf", "new/path.pdf");
// Copy file
await storage.CopyFileAsync("source.pdf", "backup/source.pdf");
// Delete file
await storage.DeleteFileAsync("file.pdf");
// Delete multiple files by pattern
int deleted = await storage.DeleteFilesAsync("temp/*");Listing Files
// List all files
var files = await storage.GetFileListAsync();
foreach (var file in files)
{
Console.WriteLine($"{file.Path} - {file.Size} bytes");
}
// List with pattern
var pdfFiles = await storage.GetFileListAsync("documents/*.pdf");
// Paged listing for large directories
var result = await storage.GetPagedFileListAsync(pageSize: 100, "logs/*");
do
{
foreach (var file in result.Files)
{
Console.WriteLine(file.Path);
}
} while (await result.NextPageAsync());Search Patterns
The searchPattern parameter in GetFileListAsync, GetPagedFileListAsync, and DeleteFilesAsync uses wildcard syntax — not regex.
Supported wildcards:
*— matches any sequence of characters, including directory separators (making searches recursive by default)?— matches any single character
Both / and \ are accepted as path separators and are normalized internally.
| Pattern | Matches |
|---|---|
null or omitted | all files |
* | all files (explicit) |
archived/* | all files under archived/ at any depth |
archived/*.txt | .txt files under archived/ |
*.json | all JSON files at any depth |
*report* | any file containing "report" in the path |
logs/2024/*/* | files exactly two levels deep under logs/2024/ |
path/to/file.txt | exact path match |
// All files
var all = await storage.GetFileListAsync();
// All files under a directory (recursive)
var archived = await storage.GetFileListAsync("archived/*");
// Only .json files anywhere in storage
var jsonFiles = await storage.GetFileListAsync("*.json");
// Files matching a mid-path wildcard
var reports = await storage.GetFileListAsync("reports/2024/*/summary.pdf");Stream Modes
Control how file streams are opened:
// Read mode - for reading existing files
using var readStream = await storage.GetFileStreamAsync("file.pdf", StreamMode.Read);
// Write mode - for creating/overwriting files
using var writeStream = await storage.GetFileStreamAsync("file.pdf", StreamMode.Write);
await someData.CopyToAsync(writeStream);Common Patterns
File Upload/Download
public class FileService
{
private readonly IFileStorage _storage;
public async Task<string> UploadAsync(IFormFile file, string folder)
{
var path = $"{folder}/{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
using var stream = file.OpenReadStream();
await _storage.SaveFileAsync(path, stream);
return path;
}
public async Task<Stream> DownloadAsync(string path)
{
if (!await _storage.ExistsAsync(path))
throw new FileNotFoundException(path);
return await _storage.GetFileStreamAsync(path, StreamMode.Read);
}
}Organized File Structure
public class DocumentStorage
{
private readonly IFileStorage _storage;
public string GetPath(int tenantId, int documentId, string fileName)
{
// Organized path: tenants/{id}/documents/{year}/{month}/{id}/{filename}
var now = DateTime.UtcNow;
return $"tenants/{tenantId}/documents/{now:yyyy}/{now:MM}/{documentId}/{fileName}";
}
public async Task SaveDocumentAsync(int tenantId, int documentId,
string fileName, Stream content)
{
var path = GetPath(tenantId, documentId, fileName);
await _storage.SaveFileAsync(path, content);
}
}File Versioning
public class VersionedFileStorage
{
private readonly IFileStorage _storage;
public async Task SaveVersionAsync(string basePath, Stream content)
{
var version = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
var versionPath = $"{basePath}.v{version}";
// Save new version
await _storage.SaveFileAsync(versionPath, content);
// Update "current" pointer
await _storage.CopyFileAsync(versionPath, basePath);
}
public async Task<IEnumerable<FileSpec>> GetVersionsAsync(string basePath)
{
return await _storage.GetFileListAsync($"{basePath}.v*");
}
}Temporary File Cleanup
public class TempFileCleanup
{
private readonly IFileStorage _storage;
public async Task CleanupOldFilesAsync(TimeSpan maxAge)
{
var files = await _storage.GetFileListAsync("temp/*");
var cutoff = DateTime.UtcNow - maxAge;
foreach (var file in files)
{
if (file.Modified < cutoff)
{
await _storage.DeleteFileAsync(file.Path);
}
}
}
}Multi-Tenant Storage
public class TenantStorageFactory
{
private readonly IFileStorage _baseStorage;
public IFileStorage GetStorageForTenant(string tenantId)
{
return new ScopedFileStorage(_baseStorage, $"tenants/{tenantId}");
}
}
// Usage
var tenantStorage = _storageFactory.GetStorageForTenant("tenant-123");
await tenantStorage.SaveFileAsync("documents/report.pdf", stream);
// Actual path: tenants/tenant-123/documents/report.pdfExtension Methods
Foundatio provides helpful extension methods:
// String content
await storage.SaveFileAsync("text.txt", "Hello World");
string? text = await storage.GetFileContentsAsync("text.txt");
// Bytes
await storage.SaveFileAsync("data.bin", byteArray);
byte[]? bytes = await storage.GetFileContentsRawAsync("data.bin");
// Objects (serialized)
await storage.SaveObjectAsync("user.json", user);
var user = await storage.GetObjectAsync<User>("user.json");
// Non-paged file listing
var allFiles = await storage.GetFileListAsync();
var filtered = await storage.GetFileListAsync("*.pdf");Dependency Injection
Basic Registration
// In-memory (development)
services.AddSingleton<IFileStorage, InMemoryFileStorage>();
// Folder (local development with persistence)
services.AddSingleton<IFileStorage>(sp =>
new FolderFileStorage(o => o.Folder = "./storage")
);
// Azure Blob (production)
services.AddSingleton<IFileStorage>(sp =>
new AzureFileStorage(o => {
o.ConnectionString = configuration["Azure:StorageConnectionString"];
o.ContainerName = "files";
})
);With Scoping
services.AddSingleton<IFileStorage>(sp =>
new FolderFileStorage(o => o.Folder = "/data")
);
// Scoped storage per tenant
services.AddScoped<IFileStorage>((sp, tenantId) =>
{
var baseStorage = sp.GetRequiredService<IFileStorage>();
return new ScopedFileStorage(baseStorage, $"tenant:{tenantId}");
});Best Practices
1. Use Meaningful Paths
// ✅ Good: Organized, meaningful paths
"documents/invoices/2024/01/invoice-12345.pdf"
"users/user-123/avatars/profile.jpg"
"temp/uploads/session-abc/file.tmp"
// ❌ Bad: Flat, unclear paths
"file1.pdf"
"12345.pdf"
"abc123"2. Include Extension in Path
// ✅ Good: Extension present
await storage.SaveFileAsync("report.pdf", stream);
// ❌ Bad: No extension
await storage.SaveFileAsync("report", stream);3. Use Scoped Storage for Isolation
// Each tenant has isolated storage
var tenantStorage = new ScopedFileStorage(baseStorage, tenantId);4. Handle Missing Files
if (!await storage.ExistsAsync(path))
{
throw new FileNotFoundException($"File not found: {path}");
}
var stream = await storage.GetFileStreamAsync(path, StreamMode.Read);5. Dispose Streams Properly
// ✅ Good: Using statement
using var stream = await storage.GetFileStreamAsync(path, StreamMode.Read);
await stream.CopyToAsync(destination);
// ❌ Bad: Not disposing
var stream = await storage.GetFileStreamAsync(path, StreamMode.Read);
await stream.CopyToAsync(destination);
// stream never disposed!6. Use Appropriate Storage for Use Case
| Use Case | Recommended Storage |
|---|---|
| Development/Testing | InMemoryFileStorage |
| Local persistence | FolderFileStorage |
| Cloud applications | AzureFileStorage, S3FileStorage |
| Multi-cloud | MinioFileStorage |
| Legacy systems | SshNetFileStorage |
FileSpec Properties
public class FileSpec
{
public required string Path { get; set; }
public long Size { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
}Next Steps
- Caching - Cache file metadata
- Jobs - Background file processing
- Azure Implementation - Production Azure setup
- AWS Implementation - Production AWS setup