ktsu.FileSystemProvider
1.0.1
Prefix Reserved
dotnet add package ktsu.FileSystemProvider --version 1.0.1
NuGet\Install-Package ktsu.FileSystemProvider -Version 1.0.1
<PackageReference Include="ktsu.FileSystemProvider" Version="1.0.1" />
<PackageVersion Include="ktsu.FileSystemProvider" Version="1.0.1" />
<PackageReference Include="ktsu.FileSystemProvider" />
paket add ktsu.FileSystemProvider --version 1.0.1
#r "nuget: ktsu.FileSystemProvider, 1.0.1"
#addin nuget:?package=ktsu.FileSystemProvider&version=1.0.1
#tool nuget:?package=ktsu.FileSystemProvider&version=1.0.1
ktsu.FileSystemProvider
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
using Microsoft.Extensions.DependencyInjection;
using ktsu.FileSystemProvider;
// Register services
var services = new ServiceCollection();
services.AddFileSystemProvider();
services.AddTransient<DocumentService>();
var serviceProvider = services.BuildServiceProvider();
Basic Service
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);
}
}
๐ 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 instancesResetToDefault()
- Resets to the default production filesystem
Extension Methods
ServiceCollection Extensions
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
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.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();
File Processing Service
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);
}
}
}
Async Operations
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;
}
}
}
Complex Dependencies
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();
}
๐งช Testing
Basic Unit Test
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();
}
}
Test Setup with Factory
[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"));
}
}
Testing with Pre-populated FileSystem
[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();
}
Parallel Test Isolation
[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"));
});
}
Simple Test Pattern
[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...
}
}
๐ง Advanced Usage
Custom Factory Registration
services.AddFileSystemProvider(serviceProvider =>
{
// Create a custom configured provider
var provider = new FileSystemProvider();
// You could configure it here if needed
// provider.SetFileSystemFactory(() => customFileSystem);
return provider;
});
Quick Testing Pattern
[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);
}
๐๏ธ 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(() => new MockFileSystem());
// To allow test mode in production (not recommended):
var provider = new FileSystemProvider(new FileSystemProviderOptions
{
ThrowOnTestModeInProduction = false
});
Lazy Initialization
The default filesystem instance is created using Lazy<T>
to ensure thread-safe, one-time initialization:
private readonly Lazy<IFileSystem> _defaultInstance = new(() => new FileSystem());
Async Context Isolation
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
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.cs
var services = new ServiceCollection();
// Register FileSystemProvider
services.AddFileSystemProvider();
// Register your services
services.AddTransient<DocumentService>();
services.AddScoped<FileProcessor>();
var serviceProvider = services.BuildServiceProvider();
2. Constructor Injection
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...
}
}
3. Test Setup
[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...
}
}
๐ฏ 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
- Register as singleton: Use
services.AddFileSystemProvider()
to register as singleton - Inject interface: Always inject
IFileSystemProvider
in 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
๐ง 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.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.6)
- TestableIO.System.IO.Abstractions (>= 22.0.14)
- TestableIO.System.IO.Abstractions.Wrappers (>= 22.0.14)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on ktsu.FileSystemProvider:
Package | Downloads |
---|---|
ktsu.PersistenceProvider
A generic persistence provider library for .NET that supports multiple storage backends (memory, file system, application data, and temporary storage). Features type-safe persistence with dependency injection support, async/await operations, and integration with ktsu.SerializationProvider and ktsu.FileSystemProvider libraries. |
GitHub repositories
This package is not used by any popular GitHub repositories.
## v1.0.1 (patch)\n\nChanges since v1.0.0:\n\n- Implement FileSystemProvider with Configuration Options and Custom Exception Handling ([@matt-edmondson](https://github.com/matt-edmondson))\n- Enhance README.md with Production Safety Features ([@matt-edmondson](https://github.com/matt-edmondson))\n