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

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:

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:

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:

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

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

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

csharp
// dotnet add package Foundatio.Redis

using Foundatio.Redis.Storage;

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

MinioFileStorage

Minio object storage (separate package):

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

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.GetFileBytesAsync("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());

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.GetFileBytesAsync("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

csharp
public class FileSpec
{
    public 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.