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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ktsu.FileSystemProvider" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ktsu.FileSystemProvider" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="ktsu.FileSystemProvider" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ktsu.FileSystemProvider --version 1.0.1
                    
#r "nuget: ktsu.FileSystemProvider, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#addin nuget:?package=ktsu.FileSystemProvider&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=ktsu.FileSystemProvider&version=1.0.1
                    
Install as a Cake Tool

ktsu.FileSystemProvider

NuGet Build Status License: 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

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

  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.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
1.0.1 135 6/18/2025
1.0.0 116 6/18/2025

## 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