Skip to content

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 ​

View source

csharp
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:

View source

csharp
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:

View source

csharp
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:

View source

csharp
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):

View source

csharp
// 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):

View source

csharp
// 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):

View source

csharp
// dotnet add package Foundatio.Redis

using Foundatio.Redis.Storage;

var storage = new RedisFileStorage(o => {
    o.ConnectionMultiplexer = redis;
});

MinioFileStorage ​

Minio object storage (separate package):

View source

csharp
// 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):

View source

csharp
// 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 ​

csharp
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 ​

csharp
// 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 ​

csharp
// 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 ​

csharp
// 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 ​

csharp
// 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.

PatternMatches
null or omittedall files
*all files (explicit)
archived/*all files under archived/ at any depth
archived/*.txt.txt files under archived/
*.jsonall 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.txtexact path match
csharp
// 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:

csharp
// 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 ​

csharp
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 ​

csharp
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 ​

csharp
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 ​

csharp
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 ​

csharp
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.pdf

Extension Methods ​

Foundatio provides helpful extension methods:

csharp
// 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 ​

csharp
// 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 ​

csharp
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 ​

csharp
// ✅ 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 ​

csharp
// ✅ Good: Extension present
await storage.SaveFileAsync("report.pdf", stream);

// ❌ Bad: No extension
await storage.SaveFileAsync("report", stream);

3. Use Scoped Storage for Isolation ​

csharp
// Each tenant has isolated storage
var tenantStorage = new ScopedFileStorage(baseStorage, tenantId);

4. Handle Missing Files ​

csharp
if (!await storage.ExistsAsync(path))
{
    throw new FileNotFoundException($"File not found: {path}");
}

var stream = await storage.GetFileStreamAsync(path, StreamMode.Read);

5. Dispose Streams Properly ​

csharp
// ✅ 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 CaseRecommended Storage
Development/TestingInMemoryFileStorage
Local persistenceFolderFileStorage
Cloud applicationsAzureFileStorage, S3FileStorage
Multi-cloudMinioFileStorage
Legacy systemsSshNetFileStorage

FileSpec Properties ​

View source

csharp
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 ​

Released under the Apache 2.0 License.