Skip to content
This repository was archived by the owner on Jun 30, 2026. It is now read-only.

Latest commit

History

104 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

ktsu.FileSystemProvider

NuGetBuild StatusLicense: MIT

A clean, dependency injection-first provider for filesystem access in .NET applications using System.IO.Abstractions.

✨ Features

  • 🔧 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

🚀 Quick Start

Installation

dotnet add package ktsu.FileSystemProvider

Basic Setup

usingMicrosoft.Extensions.DependencyInjection;usingktsu.FileSystemProvider;// Register servicesvarservices=newServiceCollection();services.AddFileSystemProvider();services.AddTransient<DocumentService>();varserviceProvider=services.BuildServiceProvider();

Basic Service

publicclassDocumentService{privatereadonlyIFileSystemProvider_fileSystemProvider;publicDocumentService(IFileSystemProviderfileSystemProvider){_fileSystemProvider=fileSystemProvider;}publicvoidSaveDocument(stringpath,stringcontent){_fileSystemProvider.Current.File.WriteAllText(path,content);}publicstringLoadDocument(stringpath){return_fileSystemProvider.Current.File.ReadAllText(path);}}

📖 API Reference

IFileSystemProvider Interface

Properties

  • Current - Gets the current filesystem instance (IFileSystem)
  • IsInTestMode - Gets whether the provider is currently in test mode (i.e., a factory has been set)

Methods

  • SetFileSystemFactory(Func<IFileSystem> factory) - Sets a factory for creating test filesystem instances
  • ResetToDefault() - Resets to the default production filesystem

Extension Methods

ServiceCollection Extensions

  • AddFileSystemProvider() - Registers FileSystemProvider as singleton
  • AddFileSystemProvider(FileSystemProviderOptions options) - Registers FileSystemProvider with configuration options
  • AddFileSystemProvider(Action<FileSystemProviderOptions> configureOptions) - Registers FileSystemProvider with configuration action
  • AddFileSystemProvider(Func<IServiceProvider, IFileSystemProvider> factory) - Registers with custom factory

Configuration Options

FileSystemProviderOptions

  • ThrowOnTestModeInProduction (bool, default: true) - Whether to throw an exception when test mode is used in production environments

💼 Production Usage

Service Registration

// 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();

File Processing Service

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);}}}

Async Operations

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;}}}

Complex Dependencies

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();}

🧪 Testing

Basic Unit Test

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();}}

Test Setup with Factory

[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"));}}

Testing with Pre-populated FileSystem

[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();}

Parallel Test Isolation

[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"));});}

Simple Test Pattern

[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...}}

🔧 Advanced Usage

Custom Factory Registration

services.AddFileSystemProvider(serviceProvider =>{// Create a custom configured providervarprovider=newFileSystemProvider();// You could configure it here if needed// provider.SetFileSystemFactory(() => customFileSystem);returnprovider;});

Quick Testing Pattern

[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);}

🏗️ Implementation Details

Production Safety

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});

Lazy Initialization

The default filesystem instance is created using Lazy<T> to ensure thread-safe, one-time initialization:

privatereadonlyLazy<IFileSystem>_defaultInstance=new(()=>newFileSystem());

Async Context Isolation

Each async context gets its own filesystem instance when using test factories:

privateFunc<IFileSystem>?_testFactory;// Shared across all contextsprivateAsyncLocal<IFileSystem?>_asyncLocalCache=new();// Cached per context

Singleton Registration

The provider is registered as a singleton in the DI container, but test factories create isolated instances per async context for proper test isolation.

📋 Best Practices

1. Service Registration

// Program.cs or Startup.csvarservices=newServiceCollection();// Register FileSystemProviderservices.AddFileSystemProvider();// Register your servicesservices.AddTransient<DocumentService>();services.AddScoped<FileProcessor>();varserviceProvider=services.BuildServiceProvider();

2. Constructor Injection

publicclassDocumentService{privatereadonlyIFileSystemProvider_fileSystemProvider;publicDocumentService(IFileSystemProviderfileSystemProvider){_fileSystemProvider=fileSystemProvider;}publicvoidProcessFile(stringpath){varcontent=_fileSystemProvider.Current.File.ReadAllText(path);// Process content...}}

3. Test Setup

[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...}}

🎯 Design Principles

  • 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

📝 Quick Reference

  1. Register as singleton: Use services.AddFileSystemProvider() to register as singleton
  2. Inject interface: Always inject IFileSystemProvider in constructors
  3. Use Current property: Access filesystem through provider.Current
  4. Test with factories: Use SetFileSystemFactory() for testing with mock filesystems
  5. Clean up tests: Call ResetToDefault() in test cleanup to restore production filesystem

🔧 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

This project is licensed under the MIT License - see the LICENSE.md file for details.

Releases

Packages

Contributors

Languages