A clean, dependency injection-first provider for filesystem access in .NET applications using System.IO.Abstractions.
- 🔧 Thread-Safe: Uses
AsyncLocal<T>for safe concurrent access across async contexts - 🧪 Testable: Factory pattern for creating isolated mock filesystems in tests
- ⚡ Lazy Initialization: Default filesystem instance is created only when needed
- 🎯 Clean API: Single interface focused on dependency injection
- 🔄 Context Isolation: Each async context gets its own filesystem instance when testing
- 🏭 Testing-Focused: Factory pattern specifically designed for test isolation
- 🛡️ Production Safe: Prevents accidental test mode usage in production environments
- 📦 Zero Configuration: Works out of the box with sensible defaults
- 🔗 DI Integration: Built for Microsoft.Extensions.DependencyInjection
dotnet add package ktsu.FileSystemProviderusingMicrosoft.Extensions.DependencyInjection;usingktsu.FileSystemProvider;// Register servicesvarservices=newServiceCollection();services.AddFileSystemProvider();services.AddTransient<DocumentService>();varserviceProvider=services.BuildServiceProvider();publicclassDocumentService{privatereadonlyIFileSystemProvider_fileSystemProvider;publicDocumentService(IFileSystemProviderfileSystemProvider){_fileSystemProvider=fileSystemProvider;}publicvoidSaveDocument(stringpath,stringcontent){_fileSystemProvider.Current.File.WriteAllText(path,content);}publicstringLoadDocument(stringpath){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 filesystem
AddFileSystemProvider()- 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 factory
ThrowOnTestModeInProduction(bool, default:true) - Whether to throw an exception when test mode is used in production environments
// Program.cs or Startup.csvarservices=newServiceCollection();// Register FileSystemProvider (default configuration)services.AddFileSystemProvider();// Or register with custom configurationservices.AddFileSystemProvider(options =>{options.ThrowOnTestModeInProduction=false;// Allow test mode in production (not recommended)});// Or register with options objectvaroptions=newFileSystemProviderOptions{ThrowOnTestModeInProduction=true// Default: true};services.AddFileSystemProvider(options);// Register your servicesservices.AddTransient<DocumentService>();services.AddScoped<FileProcessor>();varserviceProvider=services.BuildServiceProvider();publicclassFileProcessorService{privatereadonlyIFileSystemProvider_fileSystemProvider;privatereadonlyILogger<FileProcessorService>_logger;publicFileProcessorService(IFileSystemProviderfileSystemProvider,ILogger<FileProcessorService>logger){_fileSystemProvider=fileSystemProvider;_logger=logger;}publicvoidProcessFiles(stringdirectoryPath){varfiles=_fileSystemProvider.Current.Directory.GetFiles(directoryPath);foreach(varfileinfiles){varcontent=_fileSystemProvider.Current.File.ReadAllText(file);// Process file content..._logger.LogInformation("Processed {FileName}",file);}}}publicclassAsyncFileProcessor{privatereadonlyIFileSystemProvider_fileSystemProvider;privatereadonlyILogger<AsyncFileProcessor>_logger;publicAsyncFileProcessor(IFileSystemProviderfileSystemProvider,ILogger<AsyncFileProcessor>logger){_fileSystemProvider=fileSystemProvider;_logger=logger;}publicasyncTaskProcessDirectoryAsync(stringdirectoryPath){try{varfiles=_fileSystemProvider.Current.Directory.GetFiles(directoryPath,"*.txt");foreach(varfileinfiles){_logger.LogInformation("Processing file: {FileName}",file);varcontent=await_fileSystemProvider.Current.File.ReadAllTextAsync(file);varprocessedContent=content.ToUpperInvariant();varoutputFile=Path.ChangeExtension(file,".processed.txt");await_fileSystemProvider.Current.File.WriteAllTextAsync(outputFile,processedContent);_logger.LogInformation("Completed processing: {FileName}",file);}}catch(Exceptionex){_logger.LogError(ex,"Error processing directory: {DirectoryPath}",directoryPath);throw;}}}publicclassDocumentProcessor{privatereadonlyIFileSystemProvider_fileSystemProvider;privatereadonlyILogger<DocumentProcessor>_logger;privatereadonlyIConfiguration_configuration;publicDocumentProcessor(IFileSystemProviderfileSystemProvider,ILogger<DocumentProcessor>logger,IConfigurationconfiguration){_fileSystemProvider=fileSystemProvider;_logger=logger;_configuration=configuration;}publicasyncTaskProcessDocumentsAsync(){varinputPath=_configuration["DocumentProcessor:InputPath"];varoutputPath=_configuration["DocumentProcessor:OutputPath"];varfiles=_fileSystemProvider.Current.Directory.GetFiles(inputPath,"*.txt");foreach(varfileinfiles){_logger.LogInformation("Processing {FileName}",file);varcontent=await_fileSystemProvider.Current.File.ReadAllTextAsync(file);varprocessed=ProcessContent(content);varoutputFile=Path.Combine(outputPath,Path.GetFileName(file));await_fileSystemProvider.Current.File.WriteAllTextAsync(outputFile,processed);}}privatestringProcessContent(stringcontent)=>content.ToUpperInvariant();}usingSystem.Collections.Generic;usingSystem.IO.Abstractions.TestingHelpers;usingktsu.FileSystemProvider;usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.VisualStudio.TestTools.UnitTesting;[TestClass]publicclassDocumentServiceTests{[TestMethod]publicvoidSaveDocument_CreatesFile_Successfully(){// Arrangevarservices=newServiceCollection();services.AddFileSystemProvider();services.AddTransient<DocumentService>();usingvarserviceProvider=services.BuildServiceProvider();varprovider=serviceProvider.GetRequiredService<IFileSystemProvider>();provider.SetFileSystemFactory(()=>newMockFileSystem());// ActvardocumentService=serviceProvider.GetRequiredService<DocumentService>();documentService.SaveDocument("test.txt","Hello World!");// Assertvarcontent=provider.Current.File.ReadAllText("test.txt");Assert.AreEqual("Hello World!",content);// Cleanupprovider.ResetToDefault();}}[TestClass]publicclassDocumentServiceTests{privateIServiceProvider_serviceProvider=null!;privateIFileSystemProvider_fileSystemProvider=null!;[TestInitialize]publicvoidSetup(){varservices=newServiceCollection();services.AddFileSystemProvider();services.AddTransient<DocumentService>();_serviceProvider=services.BuildServiceProvider();_fileSystemProvider=_serviceProvider.GetRequiredService<IFileSystemProvider>();// Set up mock filesystem for all tests_fileSystemProvider.SetFileSystemFactory(()=>newMockFileSystem());}[TestCleanup]publicvoidCleanup(){_fileSystemProvider.ResetToDefault();_serviceProvider.Dispose();}[TestMethod]publicvoidLoadDocument_ReturnsContent_WhenFileExists(){// Arrange_fileSystemProvider.Current.File.WriteAllText("test.txt","Test Content");vardocumentService=_serviceProvider.GetRequiredService<DocumentService>();// Actvarcontent=documentService.LoadDocument("test.txt");// AssertAssert.AreEqual("Test Content",content);}[TestMethod]publicvoidProcessFiles_HandlesMultipleFiles_Successfully(){// Arrange_fileSystemProvider.Current.File.WriteAllText("C:\\data\\file1.txt","Content 1");_fileSystemProvider.Current.File.WriteAllText("C:\\data\\file2.txt","Content 2");vardocumentService=_serviceProvider.GetRequiredService<DocumentService>();// ActdocumentService.ProcessFiles();// This should not throw// AssertAssert.IsTrue(_fileSystemProvider.Current.File.Exists("C:\\data\\file1.txt"));Assert.IsTrue(_fileSystemProvider.Current.File.Exists("C:\\data\\file2.txt"));}}[TestMethod]publicvoidProcessExistingFiles_Works(){// ArrangevarmockFileSystem=newMockFileSystem(newDictionary<string,MockFileData>{{"C:\\data\\document1.txt",newMockFileData("Document 1 content")},{"C:\\data\\document2.txt",newMockFileData("Document 2 content")},{"C:\\config\\settings.json",newMockFileData("{\"setting\": \"value\"}")}});varservices=newServiceCollection();services.AddFileSystemProvider();services.AddTransient<DocumentService>();usingvarserviceProvider=services.BuildServiceProvider();varprovider=serviceProvider.GetRequiredService<IFileSystemProvider>();provider.SetFileSystemFactory(()=>mockFileSystem);// ActvardocumentService=serviceProvider.GetRequiredService<DocumentService>();varcontent=documentService.LoadDocument("C:\\data\\document1.txt");// AssertAssert.AreEqual("Document 1 content",content);// Cleanupprovider.ResetToDefault();}[TestMethod]publicvoidParallelTests_AreIsolated(){// Arrangevarprovider=newFileSystemProvider();provider.SetFileSystemFactory(()=>newMockFileSystem());// Act - Run parallel testsParallel.For(0,10, i =>{varfileSystem=provider.Current;fileSystem.File.WriteAllText($"test{i}.txt",$"content{i}");// Each parallel execution gets its own MockFileSystemvarmockFS=(MockFileSystem)provider.Current;Assert.IsTrue(mockFS.File.Exists($"test{i}.txt"));Assert.AreEqual($"content{i}",mockFS.File.ReadAllText($"test{i}.txt"));});}[TestClass]publicclassMyTests{privateIFileSystemProvider_provider=null!;[TestInitialize]publicvoidSetup(){_provider=newFileSystemProvider();_provider.SetFileSystemFactory(()=>newMockFileSystem());}[TestCleanup]publicvoidCleanup(){_provider.ResetToDefault();}[TestMethod]publicvoidMyTest(){// Each test gets its own isolated MockFileSystemvarfs=_provider.Current;fs.File.WriteAllText("test.txt","content");// Test your code...}}services.AddFileSystemProvider(serviceProvider =>{// Create a custom configured providervarprovider=newFileSystemProvider();// You could configure it here if needed// provider.SetFileSystemFactory(() => customFileSystem);returnprovider;});[TestMethod]publicvoidQuickTest(){// Arrangevarprovider=newFileSystemProvider(newFileSystemProviderOptions{ThrowOnTestModeInProduction=false});Assert.IsFalse(provider.IsInTestMode);provider.SetFileSystemFactory(()=>newMockFileSystem(newDictionary<string,MockFileData>{{"test.txt",newMockFileData("Hello World")}}));Assert.IsTrue(provider.IsInTestMode);// Actvarcontent=provider.Current.File.ReadAllText("test.txt");// AssertAssert.AreEqual("Hello World",content);// Cleanupprovider.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:
- Whether a debugger is attached (
Debugger.IsAttached) - Environment variables:
ASPNETCORE_ENVIRONMENT,DOTNET_ENVIRONMENT,ENVIRONMENT - Values considered non-production: "Development", "Test", "Testing" (case-insensitive)
// This will throw InvalidOperationException in production:provider.SetFileSystemFactory(()=>newMockFileSystem());// To allow test mode in production (not recommended):varprovider=newFileSystemProvider(newFileSystemProviderOptions{ThrowOnTestModeInProduction=false});The default filesystem instance is created using Lazy<T> to ensure thread-safe, one-time initialization:
privatereadonlyLazy<IFileSystem>_defaultInstance=new(()=>newFileSystem());Each async context gets its own filesystem instance when using test factories:
privateFunc<IFileSystem>?_testFactory;// Shared across all contextsprivateAsyncLocal<IFileSystem?>_asyncLocalCache=new();// Cached per contextThe 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.csvarservices=newServiceCollection();// Register FileSystemProviderservices.AddFileSystemProvider();// Register your servicesservices.AddTransient<DocumentService>();services.AddScoped<FileProcessor>();varserviceProvider=services.BuildServiceProvider();publicclassDocumentService{privatereadonlyIFileSystemProvider_fileSystemProvider;publicDocumentService(IFileSystemProviderfileSystemProvider){_fileSystemProvider=fileSystemProvider;}publicvoidProcessFile(stringpath){varcontent=_fileSystemProvider.Current.File.ReadAllText(path);// Process content...}}[TestClass]publicclassMyTests{privateIServiceProvider_serviceProvider=null!;privateIFileSystemProvider_fileSystemProvider=null!;[TestInitialize]publicvoidSetup(){varservices=newServiceCollection();services.AddFileSystemProvider();services.AddTransient<YourService>();_serviceProvider=services.BuildServiceProvider();_fileSystemProvider=_serviceProvider.GetRequiredService<IFileSystemProvider>();// Set up mock filesystem for all tests_fileSystemProvider.SetFileSystemFactory(()=>newMockFileSystem());}[TestCleanup]publicvoidCleanup(){_fileSystemProvider.ResetToDefault();_serviceProvider.Dispose();}[TestMethod]publicvoidMyTest(){// Each test gets isolated filesystem instancevarservice=_serviceProvider.GetRequiredService<YourService>();// Test your service...}}- Dependency Injection First: Built for modern .NET applications
- No Static State: Avoids global state and service locator anti-patterns
- Test Isolation: Each test/async context gets its own filesystem
- Simple Interface: Single responsibility with minimal surface area
- Thread Safety: Safe for concurrent use across multiple threads
- Register as singleton: Use
services.AddFileSystemProvider()to register as singleton - Inject interface: Always inject
IFileSystemProviderin constructors - Use Current property: Access filesystem through
provider.Current - Test with factories: Use
SetFileSystemFactory()for testing with mock filesystems - Clean up tests: Call
ResetToDefault()in test cleanup to restore production filesystem
Contributions 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.