name: system-io-abstractions description: > Guidance for System.IO.Abstractions file system abstraction library. USE FOR: wrapping file and directory operations for testability, mocking file system access in unit tests, replacing static File/Directory/Path calls with injectable interfaces, using MockFileSystem for deterministic test scenarios, testing code that reads/writes files. DO NOT USE FOR: actual file I/O performance optimization, replacing stream-based APIs, or scenarios where you need raw file system performance without abstraction overhead. license: MIT metadata: displayName: "System.IO.Abstractions" author: "Tyler-R-Kendrick" version: "1.0.0" compatibility:
- claude
- copilot
- cursor references:
- title: "System.IO.Abstractions GitHub Repository" url: "https://github.com/TestableIO/System.IO.Abstractions"
- title: "System.IO.Abstractions NuGet Package" url: "https://www.nuget.org/packages/System.IO.Abstractions"
- title: "System.IO.Abstractions TestingHelpers NuGet Package" url: "https://www.nuget.org/packages/System.IO.Abstractions.TestingHelpers"
System.IO.Abstractions
Overview
System.IO.Abstractions provides interface wrappers around .NET's static File, Directory, Path, and FileInfo classes, making file system operations injectable and testable. Instead of calling File.ReadAllText(path) directly, you inject IFileSystem and call fileSystem.File.ReadAllText(path). The companion package System.IO.Abstractions.TestingHelpers provides MockFileSystem, an in-memory file system implementation that enables fast, deterministic unit tests without touching the real disk. This library follows the adapter pattern and is the standard approach for making file-dependent code testable in .NET.
Service Registration
Register IFileSystem in the DI container for production and testing use.
using System.IO.Abstractions;
using Microsoft.Extensions.DependencyInjection;
// Program.cs - production registration
builder.Services.AddSingleton<IFileSystem, FileSystem>();
// In tests, replace with MockFileSystem
services.AddSingleton<IFileSystem>(new MockFileSystem());
Injecting and Using IFileSystem
Replace direct File and Directory static calls with IFileSystem.
using System.IO.Abstractions;
public sealed class ConfigurationLoader
{
private readonly IFileSystem _fileSystem;
public ConfigurationLoader(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
public async Task<string> LoadConfigAsync(string path)
{
if (!_fileSystem.File.Exists(path))
throw new FileNotFoundException(
$"Configuration file not found: {path}");
return await _fileSystem.File.ReadAllTextAsync(path);
}
public async Task SaveConfigAsync(string path, string content)
{
string? directory = _fileSystem.Path.GetDirectoryName(path);
if (directory != null && !_fileSystem.Directory.Exists(directory))
_fileSystem.Directory.CreateDirectory(directory);
await _fileSystem.File.WriteAllTextAsync(path, content);
}
}
Testing with MockFileSystem
Use MockFileSystem to set up an in-memory file system for unit tests.
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using Xunit;
public class ConfigurationLoaderTests
{
[Fact]
public async Task LoadConfig_Returns_File_Contents()
{
// Arrange: create a mock file system with a pre-existing file
var mockFs = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ @"C:\app\config.json", new MockFileData("""
{
"Database": "Server=localhost;Database=mydb",
"LogLevel": "Information"
}
""") }
});
var loader = new ConfigurationLoader(mockFs);
// Act
string content = await loader.LoadConfigAsync(@"C:\app\config.json");
// Assert
Assert.Contains("Database", content);
Assert.Contains("mydb", content);
}
[Fact]
public async Task LoadConfig_Throws_When_File_Missing()
{
var mockFs = new MockFileSystem();
var loader = new ConfigurationLoader(mockFs);
await Assert.ThrowsAsync<FileNotFoundException>(
() => loader.LoadConfigAsync(@"C:\missing\config.json"));
}
[Fact]
public async Task SaveConfig_Creates_Directory_And_File()
{
var mockFs = new MockFileSystem();
var loader = new ConfigurationLoader(mockFs);
await loader.SaveConfigAsync(
@"C:\app\settings\config.json",
"""{"setting": "value"}""");
Assert.True(mockFs.Directory.Exists(@"C:\app\settings"));
Assert.True(mockFs.File.Exists(@"C:\app\settings\config.json"));
string saved = await mockFs.File.ReadAllTextAsync(
@"C:\app\settings\config.json");
Assert.Contains("value", saved);
}
}
File Processing Service
Build a complete file processing service with full testability.
using System.IO.Abstractions;
using System.Text.Json;
public sealed class ReportService
{
private readonly IFileSystem _fileSystem;
public ReportService(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
public async Task<List<string>> GetReportFilesAsync(string directory)
{
if (!_fileSystem.Directory.Exists(directory))
return new List<string>();
return _fileSystem.Directory
.GetFiles(directory, "*.json")
.OrderBy(f => _fileSystem.FileInfo
.New(f).CreationTimeUtc)
.ToList();
}
public async Task<T?> ReadReportAsync<T>(string path)
{
string json = await _fileSystem.File.ReadAllTextAsync(path);
return JsonSerializer.Deserialize<T>(json);
}
public async Task WriteReportAsync<T>(
string directory, string fileName, T report)
{
if (!_fileSystem.Directory.Exists(directory))
_fileSystem.Directory.CreateDirectory(directory);
string path = _fileSystem.Path.Combine(directory, fileName);
string json = JsonSerializer.Serialize(report,
new JsonSerializerOptions { WriteIndented = true });
await _fileSystem.File.WriteAllTextAsync(path, json);
}
public long GetDirectorySizeBytes(string directory)
{
if (!_fileSystem.Directory.Exists(directory))
return 0;
return _fileSystem.Directory
.GetFiles(directory, "*", SearchOption.AllDirectories)
.Sum(f => _fileSystem.FileInfo.New(f).Length);
}
}
Testing File Processing
Test the report service with various mock file system states.
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using Xunit;
public class ReportServiceTests
{
[Fact]
public async Task GetReportFiles_Returns_Only_Json_Files()
{
var mockFs = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ @"C:\reports\jan.json", new MockFileData("{}") },
{ @"C:\reports\feb.json", new MockFileData("{}") },
{ @"C:\reports\readme.txt", new MockFileData("text") },
{ @"C:\reports\data.csv", new MockFileData("a,b,c") }
});
var service = new ReportService(mockFs);
var files = await service.GetReportFilesAsync(@"C:\reports");
Assert.Equal(2, files.Count);
Assert.All(files, f => Assert.EndsWith(".json", f));
}
[Fact]
public async Task GetReportFiles_Returns_Empty_For_Missing_Directory()
{
var mockFs = new MockFileSystem();
var service = new ReportService(mockFs);
var files = await service.GetReportFilesAsync(@"C:\nonexistent");
Assert.Empty(files);
}
[Fact]
public void GetDirectorySize_Calculates_Total_Bytes()
{
var mockFs = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ @"C:\data\file1.bin", new MockFileData(new byte[1024]) },
{ @"C:\data\file2.bin", new MockFileData(new byte[2048]) },
{ @"C:\data\sub\file3.bin", new MockFileData(new byte[512]) }
});
var service = new ReportService(mockFs);
long size = service.GetDirectorySizeBytes(@"C:\data");
Assert.Equal(3584, size); // 1024 + 2048 + 512
}
[Fact]
public async Task WriteReport_Then_ReadReport_RoundTrips()
{
var mockFs = new MockFileSystem();
var service = new ReportService(mockFs);
var report = new { Title = "Monthly", Value = 42.5 };
await service.WriteReportAsync(
@"C:\reports", "output.json", report);
Assert.True(mockFs.File.Exists(@"C:\reports\output.json"));
}
}
Wrapping Path Operations
Use IPath for path manipulation to support cross-platform testing.
using System.IO.Abstractions;
public sealed class PathResolver
{
private readonly IFileSystem _fileSystem;
private readonly string _basePath;
public PathResolver(IFileSystem fileSystem, string basePath)
{
_fileSystem = fileSystem;
_basePath = basePath;
}
public string Resolve(string relativePath)
{
string combined = _fileSystem.Path.Combine(
_basePath, relativePath);
return _fileSystem.Path.GetFullPath(combined);
}
public string GetExtension(string fileName) =>
_fileSystem.Path.GetExtension(fileName);
public string ChangeExtension(string path, string newExtension) =>
_fileSystem.Path.ChangeExtension(path, newExtension);
public bool IsWithinBasePath(string path)
{
string fullPath = _fileSystem.Path.GetFullPath(path);
return fullPath.StartsWith(_basePath,
StringComparison.OrdinalIgnoreCase);
}
}
API Comparison
| Static API | IFileSystem Equivalent | Test Mock |
|---|---|---|
File.ReadAllText(path) |
fileSystem.File.ReadAllText(path) |
MockFileSystem with pre-loaded files |
File.Exists(path) |
fileSystem.File.Exists(path) |
Files in MockFileSystem dictionary |
Directory.CreateDirectory(path) |
fileSystem.Directory.CreateDirectory(path) |
Verified via mockFs.Directory.Exists() |
Path.Combine(a, b) |
fileSystem.Path.Combine(a, b) |
Works identically in mock |
new FileInfo(path) |
fileSystem.FileInfo.New(path) |
MockFileData with size/dates |
File.WriteAllTextAsync(path, text) |
fileSystem.File.WriteAllTextAsync(path, text) |
Read back from MockFileSystem |
Best Practices
- Inject
IFileSystemeverywhere instead of using staticFileandDirectorycalls: this single change makes all file-dependent code unit testable without touching the real disk. - Register
FileSystemas a singleton in production: the realFileSystemimplementation is stateless and thread-safe; there is no need for scoped or transient registration. - Pre-populate
MockFileSystemwith test data in the constructor: pass aDictionary<string, MockFileData>to set up the initial file system state rather than creating files in test Arrange steps. - Test both file-exists and file-missing scenarios: always verify behavior when expected files are absent, directories do not exist, or paths are invalid.
- Use
IFileSystem.Pathinstead ofSystem.IO.Pathdirectly: whilePathmethods are mostly static math, wrapping them throughIFileSystem.Pathmaintains consistency and supports cross-platform test scenarios. - Use
MockFileDatawith byte arrays for binary file testing:new MockFileData(new byte[1024])creates a mock file with specific size for testing size calculations and binary reads. - Avoid mixing
IFileSystemcalls with rawSystem.IOcalls in the same class: if a method usesIFileSystem.File.ReadAllText, do not also callFile.Existsstatically, as the mock will not intercept the static call. - Use
IFileInfoandIDirectoryInfofor metadata access: when you need creation time, last write time, or file size, usefileSystem.FileInfo.New(path)instead ofnew FileInfo(path). - Test path traversal prevention with
MockFileSystem: verify thatGetFullPathcombined with aStartsWithcheck on the base directory prevents../attacks in your path resolution logic. - Install
System.IO.Abstractions.TestingHelpersonly in test projects: the mainSystem.IO.Abstractionspackage goes in your production projects; theTestingHelperspackage withMockFileSystemshould only be referenced by test projects.