ktsu.FileSystemProvider 1.0.4

Prefix Reserved
dotnet add package ktsu.FileSystemProvider --version 1.0.4
                    
NuGet\Install-Package ktsu.FileSystemProvider -Version 1.0.4
                    
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.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ktsu.FileSystemProvider" Version="1.0.4" />
                    
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.4
                    
#r "nuget: ktsu.FileSystemProvider, 1.0.4"
                    
#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.
#:package ktsu.FileSystemProvider@1.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ktsu.FileSystemProvider&version=1.0.4
                    
Install as a Cake Addin
#tool nuget:?package=ktsu.FileSystemProvider&version=1.0.4
                    
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 net5.0 is compatible.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  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 is compatible.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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.4 99 6/28/2026
1.0.3 97 6/28/2026
1.0.3-pre.3 189 11/24/2025
1.0.3-pre.2 153 11/23/2025
1.0.3-pre.1 134 11/23/2025
1.0.2 430 11/14/2025
1.0.2-pre.2 576 7/22/2025
1.0.2-pre.1 172 7/9/2025
1.0.1 612 6/18/2025
1.0.0 217 6/18/2025

## v1.0.4 (patch)

Changes since v1.0.3:

- Merge remote-tracking branch 'refs/remotes/origin/main' ([@ktsu[bot]](https://github.com/ktsu[bot]))
- Sync .github\workflows\dotnet.yml ([@ktsu[bot]](https://github.com/ktsu[bot]))
- Sync global.json ([@ktsu[bot]](https://github.com/ktsu[bot]))

## v1.0.3 (patch)

Changes since v1.0.2:

- fix: validate file system factory result without throw expression ([@matt-edmondson](https://github.com/matt-edmondson))
- refactor: migrate to new SDK and filesystem abstractions ([@matt-edmondson](https://github.com/matt-edmondson))
- Remove legacy build scripts ([@matt-edmondson](https://github.com/matt-edmondson))

## v1.0.3-pre.3 (prerelease)

Changes since v1.0.3-pre.2:

- Sync scripts\update-winget-manifests.ps1 ([@ktsu[bot]](https://github.com/ktsu[bot]))

## v1.0.3-pre.2 (prerelease)

Changes since v1.0.3-pre.1:

- Sync .github\workflows\dotnet.yml ([@ktsu[bot]](https://github.com/ktsu[bot]))

## v1.0.3-pre.1 (prerelease)

No significant changes detected since v1.0.3.

## v1.0.2 (patch)

Changes since v1.0.1:

- Update project configurations and workflows ([@matt-edmondson](https://github.com/matt-edmondson))
- Update package versions and improve changelog handling in PSBuild script ([@matt-edmondson](https://github.com/matt-edmondson))

## v1.0.2-pre.2 (prerelease)

Changes since v1.0.2-pre.1:

- Update package versions and improve changelog handling in PSBuild script ([@matt-edmondson](https://github.com/matt-edmondson))

## v1.0.2-pre.1 (prerelease)

No significant changes detected since v1.0.2.

## v1.0.1 (patch)

Changes since v1.0.0:

- Enhance README.md with Production Safety Features ([@matt-edmondson](https://github.com/matt-edmondson))
- Implement FileSystemProvider with Configuration Options and Custom Exception Handling ([@matt-edmondson](https://github.com/matt-edmondson))

## v1.0.0 (major)

- Refactor FileSystemProvider for Dependency Injection and Modern Features ([@matt-edmondson](https://github.com/matt-edmondson))
- Initial implementation ([@matt-edmondson](https://github.com/matt-edmondson))