A clean, dependency injection-first provider for filesystem access in .NET applications using System.IO.Abstractions. Features thread-safe operations, async context isolation, lazy initialization, and factory pattern for easy testing with mock filesystems. Built for modern .NET dependency injection.
$ dotnet add package ktsu.FileSystemProviderA clean, dependency injection-first provider for filesystem access in .NET applications using System.IO.Abstractions.
AsyncLocal<T> for safe concurrent access across async contextsdotnet add package ktsu.FileSystemProvider
using Microsoft.Extensions.DependencyInjection;
using ktsu.FileSystemProvider;
// Register services
var services = new ServiceCollection();
services.AddFileSystemProvider();
services.AddTransient<DocumentService>();
var serviceProvider = services.BuildServiceProvider();
public class DocumentService
{
private readonly IFileSystemProvider _fileSystemProvider;
public DocumentService(IFileSystemProvider fileSystemProvider)
{
_fileSystemProvider = fileSystemProvider;
}
public void SaveDocument(string path, string content)
{
_fileSystemProvider.Current.File.WriteAllText(path, content);
}
public string LoadDocument(string path)
{
return _fileSystemProvider.Current.File.ReadAllText(path);
}
}
Current - Gets the current filesystem instance (IFileSystem)IsInTestMode - Gets whether the provider is currently in test mode (i.e., a factory has been set)SetFileSystemFactory(Func<IFileSystem> factory) - Sets a factory for creating test filesystem instancesResetToDefault() - Resets to the default production filesystemAddFileSystemProvider() - Registers FileSystemProvider as singletonAddFileSystemProvider(FileSystemProviderOptions options) - Registers FileSystemProvider with configuration optionsAddFileSystemProvider(Action<FileSystemProviderOptions> configureOptions) - Registers FileSystemProvider with configuration actionAddFileSystemProvider(Func<IServiceProvider, IFileSystemProvider> factory) - Registers with custom factoryThrowOnTestModeInProduction (bool, default: true) - Whether to throw an exception when test mode is used in production environments// Program.cs or Startup.cs
var services = new ServiceCollection();
// Register FileSystemProvider (default configuration)
services.AddFileSystemProvider();
// Or register with custom configuration
services.AddFileSystemProvider(options =>
{
options.ThrowOnTestModeInProduction = false; // Allow test mode in production (not recommended)
});
// Or register with options object
var options = new FileSystemProviderOptions
{
ThrowOnTestModeInProduction = true // Default: true
};
services.AddFileSystemProvider(options);
// Register your services
services.AddTransient<DocumentService>();
services.AddScoped<FileProcessor>();
var serviceProvider = services.BuildServiceProvider();
public class FileProcessorService
{
private readonly IFileSystemProvider _fileSystemProvider;
private readonly ILogger<FileProcessorService> _logger;
public FileProcessorService(
IFileSystemProvider fileSystemProvider,
ILogger<FileProcessorService> logger)
{
_fileSystemProvider = fileSystemProvider;
_logger = logger;
}
public void ProcessFiles(string directoryPath)
{
var files = _fileSystemProvider.Current.Directory.GetFiles(directoryPath);
foreach (var file in files)
{
var content = _fileSystemProvider.Current.File.ReadAllText(file);
// Process file content...
_logger.LogInformation("Processed {FileName}", file);
}
}
}
public class AsyncFileProcessor
{
private readonly IFileSystemProvider _fileSystemProvider;
private readonly ILogger<AsyncFileProcessor> _logger;
public AsyncFileProcessor(
IFileSystemProvider fileSystemProvider,
ILogger<AsyncFileProcessor> logger)
{
_fileSystemProvider = fileSystemProvider;
_logger = logger;
}
public async Task ProcessDirectoryAsync(string directoryPath)
{
try
{
var files = _fileSystemProvider.Current.Directory.GetFiles(directoryPath, "*.txt");
foreach (var file in files)
{
_logger.LogInformation("Processing file: {FileName}", file);
var content = await _fileSystemProvider.Current.File.ReadAllTextAsync(file);
var processedContent = content.ToUpperInvariant();
var outputFile = Path.ChangeExtension(file, ".processed.txt");
await _fileSystemProvider.Current.File.WriteAllTextAsync(outputFile, processedContent);
_logger.LogInformation("Completed processing: {FileName}", file);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing directory: {DirectoryPath}", directoryPath);
throw;
}
}
}
public class DocumentProcessor
{
private readonly IFileSystemProvider _fileSystemProvider;
private readonly ILogger<DocumentProcessor> _logger;
private readonly IConfiguration _configuration;
public DocumentProcessor(
IFileSystemProvider fileSystemProvider,
ILogger<DocumentProcessor> logger,
IConfiguration configuration)
{
_fileSystemProvider = fileSystemProvider;
_logger = logger;
_configuration = configuration;
}
public async Task ProcessDocumentsAsync()
{
var inputPath = _configuration["DocumentProcessor:InputPath"];
var outputPath = _configuration["DocumentProcessor:OutputPath"];
var files = _fileSystemProvider.Current.Directory.GetFiles(inputPath, "*.txt");
foreach (var file in files)
{
_logger.LogInformation("Processing {FileName}", file);
var content = await _fileSystemProvider.Current.File.ReadAllTextAsync(file);
var processed = ProcessContent(content);
var outputFile = Path.Combine(outputPath, Path.GetFileName(file));
await _fileSystemProvider.Current.File.WriteAllTextAsync(outputFile, processed);
}
}
private string ProcessContent(string content) => content.ToUpperInvariant();
}
using System.Collections.Generic;
using System.IO.Abstractions.TestingHelpers;
using ktsu.FileSystemProvider;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class DocumentServiceTests
{
[TestMethod]
public void SaveDocument_CreatesFile_Successfully()
{
// Arrange
var services = new ServiceCollection();
services.AddFileSystemProvider();
services.AddTransient<DocumentService>();
using var serviceProvider = services.BuildServiceProvider();
var provider = serviceProvider.GetRequiredService<IFileSystemProvider>();
provider.SetFileSystemFactory(() => new MockFileSystem());
// Act
var documentService = serviceProvider.GetRequiredService<DocumentService>();
documentService.SaveDocument("test.txt", "Hello World!");
// Assert
var content = provider.Current.File.ReadAllText("test.txt");
Assert.AreEqual("Hello World!", content);
// Cleanup
provider.ResetToDefault();
}
}
[TestClass]
public class DocumentServiceTests
{
private IServiceProvider _serviceProvider = null!;
private IFileSystemProvider _fileSystemProvider = null!;
[TestInitialize]
public void Setup()
{
var services = new ServiceCollection();
services.AddFileSystemProvider();
services.AddTransient<DocumentService>();
_serviceProvider = services.BuildServiceProvider();
_fileSystemProvider = _serviceProvider.GetRequiredService<IFileSystemProvider>();
// Set up mock filesystem for all tests
_fileSystemProvider.SetFileSystemFactory(() => new MockFileSystem());
}
[TestCleanup]
public void Cleanup()
{
_fileSystemProvider.ResetToDefault();
_serviceProvider.Dispose();
}
[TestMethod]
public void LoadDocument_ReturnsContent_WhenFileExists()
{
// Arrange
_fileSystemProvider.Current.File.WriteAllText("test.txt", "Test Content");
var documentService = _serviceProvider.GetRequiredService<DocumentService>();
// Act
var content = documentService.LoadDocument("test.txt");
// Assert
Assert.AreEqual("Test Content", content);
}
[TestMethod]
public void ProcessFiles_HandlesMultipleFiles_Successfully()
{
// Arrange
_fileSystemProvider.Current.File.WriteAllText("C:\\data\\file1.txt", "Content 1");
_fileSystemProvider.Current.File.WriteAllText("C:\\data\\file2.txt", "Content 2");
var documentService = _serviceProvider.GetRequiredService<DocumentService>();
// Act
documentService.ProcessFiles(); // This should not throw
// Assert
Assert.IsTrue(_fileSystemProvider.Current.File.Exists("C:\\data\\file1.txt"));
Assert.IsTrue(_fileSystemProvider.Current.File.Exists("C:\\data\\file2.txt"));
}
}
[TestMethod]
public void ProcessExistingFiles_Works()
{
// Arrange
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ "C:\\data\\document1.txt", new MockFileData("Document 1 content") },
{ "C:\\data\\document2.txt", new MockFileData("Document 2 content") },
{ "C:\\config\\settings.json", new MockFileData("{\"setting\": \"value\"}") }
});
var services = new ServiceCollection();
services.AddFileSystemProvider();
services.AddTransient<DocumentService>();
using var serviceProvider = services.BuildServiceProvider();
var provider = serviceProvider.GetRequiredService<IFileSystemProvider>();
provider.SetFileSystemFactory(() => mockFileSystem);
// Act
var documentService = serviceProvider.GetRequiredService<DocumentService>();
var content = documentService.LoadDocument("C:\\data\\document1.txt");
// Assert
Assert.AreEqual("Document 1 content", content);
// Cleanup
provider.ResetToDefault();
}
[TestMethod]
public void ParallelTests_AreIsolated()
{
// Arrange
var provider = new FileSystemProvider();
provider.SetFileSystemFactory(() => new MockFileSystem());
// Act - Run parallel tests
Parallel.For(0, 10, i =>
{
var fileSystem = provider.Current;
fileSystem.File.WriteAllText($"test{i}.txt", $"content{i}");
// Each parallel execution gets its own MockFileSystem
var mockFS = (MockFileSystem)provider.Current;
Assert.IsTrue(mockFS.File.Exists($"test{i}.txt"));
Assert.AreEqual($"content{i}", mockFS.File.ReadAllText($"test{i}.txt"));
});
}
[TestClass]
public class MyTests
{
private IFileSystemProvider _provider = null!;
[TestInitialize]
public void Setup()
{
_provider = new FileSystemProvider();
_provider.SetFileSystemFactory(() => new MockFileSystem());
}
[TestCleanup]
public void Cleanup()
{
_provider.ResetToDefault();
}
[TestMethod]
public void MyTest()
{
// Each test gets its own isolated MockFileSystem
var fs = _provider.Current;
fs.File.WriteAllText("test.txt", "content");
// Test your code...
}
}
services.AddFileSystemProvider(serviceProvider =>
{
// Create a custom configured provider
var provider = new FileSystemProvider();
// You could configure it here if needed
// provider.SetFileSystemFactory(() => customFileSystem);
return provider;
});
[TestMethod]
public void QuickTest()
{
// Arrange
var provider = new FileSystemProvider(new FileSystemProviderOptions
{
ThrowOnTestModeInProduction = false
});
Assert.IsFalse(provider.IsInTestMode);
provider.SetFileSystemFactory(() => new MockFileSystem(new Dictionary<string, MockFileData>
{
{ "test.txt", new MockFileData("Hello World") }
}));
Assert.IsTrue(provider.IsInTestMode);
// Act
var content = provider.Current.File.ReadAllText("test.txt");
// Assert
Assert.AreEqual("Hello World", content);
// Cleanup
provider.ResetToDefault();
Assert.IsFalse(provider.IsInTestMode);
}
By default, the library prevents test mode from being enabled in production environments. This is controlled by the ThrowOnTestModeInProduction setting (default: true). The library detects production environments by checking:
Debugger.IsAttached)ASPNETCORE_ENVIRONMENT, DOTNET_ENVIRONMENT, ENVIRONMENT// This will throw InvalidOperationException in production:
provider.SetFileSystemFactory(() => new MockFileSystem());
// To allow test mode in production (not recommended):
var provider = new FileSystemProvider(new FileSystemProviderOptions
{
ThrowOnTestModeInProduction = false
});
The default filesystem instance is created using Lazy<T> to ensure thread-safe, one-time initialization:
private readonly Lazy<IFileSystem> _defaultInstance = new(() => new FileSystem());
Each async context gets its own filesystem instance when using test factories:
private Func<IFileSystem>? _testFactory; // Shared across all contexts
private AsyncLocal<IFileSystem?> _asyncLocalCache = new(); // Cached per context
The provider is registered as a singleton in the DI container, but test factories create isolated instances per async context for proper test isolation.
// Program.cs or Startup.cs
var services = new ServiceCollection();
// Register FileSystemProvider
services.AddFileSystemProvider();
// Register your services
services.AddTransient<DocumentService>();
services.AddScoped<FileProcessor>();
var serviceProvider = services.BuildServiceProvider();
public class DocumentService
{
private readonly IFileSystemProvider _fileSystemProvider;
public DocumentService(IFileSystemProvider fileSystemProvider)
{
_fileSystemProvider = fileSystemProvider;
}
public void ProcessFile(string path)
{
var content = _fileSystemProvider.Current.File.ReadAllText(path);
// Process content...
}
}
[TestClass]
public class MyTests
{
private IServiceProvider _serviceProvider = null!;
private IFileSystemProvider _fileSystemProvider = null!;
[TestInitialize]
public void Setup()
{
var services = new ServiceCollection();
services.AddFileSystemProvider();
services.AddTransient<YourService>();
_serviceProvider = services.BuildServiceProvider();
_fileSystemProvider = _serviceProvider.GetRequiredService<IFileSystemProvider>();
// Set up mock filesystem for all tests
_fileSystemProvider.SetFileSystemFactory(() => new MockFileSystem());
}
[TestCleanup]
public void Cleanup()
{
_fileSystemProvider.ResetToDefault();
_serviceProvider.Dispose();
}
[TestMethod]
public void MyTest()
{
// Each test gets isolated filesystem instance
var service = _serviceProvider.GetRequiredService<YourService>();
// Test your service...
}
}
services.AddFileSystemProvider() to register as singletonIFileSystemProvider in constructorsprovider.CurrentSetFileSystemFactory() for testing with mock filesystemsResetToDefault() in test cleanup to restore production filesystemContributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE.md file for details.