FileSystem 0.5.1

A FileSystem abstraction, allowing decoration and testing of the physical file system.

Install-Package FileSystem -Version 0.5.1
dotnet add package FileSystem --version 0.5.1
paket add FileSystem --version 0.5.1
The NuGet Team does not provide support for this client. Please contact its maintainers for support.

FileSystem

An async FileSystem abstraction, with decoration and in-memory/virtual support.

Build status
NuGet

Installation

This package is available on NuGet.

PM> install-package FileSystem

Usage

public class Configuration
{
    private readonly IFileSystem _fileSystem;

    public Configuration(IFileSystem fileSystem)
    {
        _fileSystem = fileSystem;
    }

    public async Task Load()
    {
        using (var stream = await _fileSystem.ReadFile("config.json"))
        {
            //...
        }
    }

    public async Task Save()
    {
        await _fileSystem.WriteFile("config.json", async stream => {
            await stream.Write(/* ... */);
        });
    }
}

//usage:
var config = new Configuration(new PhysicalFileSystem());

Logging

As I didn't want to take a dependency on any particular logging library, there is no out of the box implementation. However, implementing your own only takes a few lines of code, making use of the EventingFileSystem class. For example, logging everything as Debug with Serilog:

public class LoggingFileSystem : EventingFileSystem
{
    public LoggingFileSystem(IFileSystem inner) : base(inner)
    {
        HandleEvent = message =>
        {
            Log.Debug($"{message}: {{@event}}", message);
            return Task.CompletedTask;
        };
    }
}

Each event emitted by the EventingFileSystem has a reasonable .ToString implementation, so you can just write Console.WriteLine(message.ToString()) if you wish.

Decoration

For ease of implementing, FileSystem supplies a FileSystemDecorator class, which implements all IFileSystem methods as virtual calls to an inner IFileSystem.

For example, an encrypting filesystem could be implemented by just overriding a few methods:

public class EncryptingFileSystem : FileSystemDecorator
{
    private readonly ICrypto _crypto;

    public EncryptingFileSystem(IFileSystem inner, ICrypto crypto) : base(inner)
    {
        _crypto = crypto;
    }

    public override  async Task<Stream> ReadFile(string path)
    {
        return await _crypto.DecryptStream(await base.ReadFile(path));
    }

    public override async Task WriteFile(string path, Func<Stream, Task> write)
    {
        await base.WriteFile(path, async stream =>
        {
            using (var encrypted = await _crypto.Encrypt(async cryptoStream => await write(cryptoStream)))
            {
                await encrypted.CopyToAsync(stream);
            }
        });
    }

    public override Task AppendFile(string path, Func<Stream, Task> write)
    {
        throw new NotSupportedException("You cannot append to an encrypted file.  Try reading, and the writing the whole file.");
    }
}

Testing

The easiest way of testing code using an IFileSystem dependency is to use the InMemoryFileSystem, which will behave the same as the physical file system.

var fileSystem = new InMemoryFileSystem();

var sut = new TestClass(fileSystem);
sut.Execute();

fileSystem
    .ReadLines("./the/file.txt")
    .ShouldBe(new[] { "first", "second", "third" });

Alternatly, if you want to assert on something written to a stream, e.g. on a .AppendFile() call, you can do it manually (this example using NSubstitute):

var ms = new MemoryStream();
var fileSystem = Substitute.For<IFileSystem>();
fileSystem
    .AppendFile("wat", Arg.Do<Func<Stream, Task>>(func => func(ms).Wait()))
    .Returns(Task.CompletedTask);

fileSystem.AppendFile("wat", async stream => {
    await stream.WriteAsync(new byte[] { 1, 2, 3 }, 0, 3);
});

ms.ToArray().ShouldBe(new byte[] { 1, 2, 3});

Or if you want to capture many streams, you can use the provided StreamCapture class:

var streams = new StreamCapture();
var fileSystem = Substitute.For<IFileSystem>();
fileSystem
    .AppendFile("wat", Arg.Do<Func<Stream, Task>>(streams.Capture))
    .Returns(Task.CompletedTask);

fileSystem.AppendFile("wat", async stream => {
    await stream.WriteAsync(new byte[] { 1, 2, 3 }, 0, 3);
});

streams.Last.ToArray().ShouldBe(new byte[] { 1, 2, 3});

To do

  • Caching FileSystem
    • Read caching I guess
    • pluggable caching strategies
  • Commitable FileSystem (call .Commit() to flush writes to disk.)
  • Case(In)Sensitive FileSystem?
    • Not sure how this would work
    • ReadFile would ListFiles first, then find the right mapping perhaps?
  • S3FileSystem
    • Separate package I guess

FileSystem

An async FileSystem abstraction, with decoration and in-memory/virtual support.

Build status
NuGet

Installation

This package is available on NuGet.

PM> install-package FileSystem

Usage

public class Configuration
{
    private readonly IFileSystem _fileSystem;

    public Configuration(IFileSystem fileSystem)
    {
        _fileSystem = fileSystem;
    }

    public async Task Load()
    {
        using (var stream = await _fileSystem.ReadFile("config.json"))
        {
            //...
        }
    }

    public async Task Save()
    {
        await _fileSystem.WriteFile("config.json", async stream => {
            await stream.Write(/* ... */);
        });
    }
}

//usage:
var config = new Configuration(new PhysicalFileSystem());

Logging

As I didn't want to take a dependency on any particular logging library, there is no out of the box implementation. However, implementing your own only takes a few lines of code, making use of the EventingFileSystem class. For example, logging everything as Debug with Serilog:

public class LoggingFileSystem : EventingFileSystem
{
    public LoggingFileSystem(IFileSystem inner) : base(inner)
    {
        HandleEvent = message =>
        {
            Log.Debug($"{message}: {{@event}}", message);
            return Task.CompletedTask;
        };
    }
}

Each event emitted by the EventingFileSystem has a reasonable .ToString implementation, so you can just write Console.WriteLine(message.ToString()) if you wish.

Decoration

For ease of implementing, FileSystem supplies a FileSystemDecorator class, which implements all IFileSystem methods as virtual calls to an inner IFileSystem.

For example, an encrypting filesystem could be implemented by just overriding a few methods:

public class EncryptingFileSystem : FileSystemDecorator
{
    private readonly ICrypto _crypto;

    public EncryptingFileSystem(IFileSystem inner, ICrypto crypto) : base(inner)
    {
        _crypto = crypto;
    }

    public override  async Task<Stream> ReadFile(string path)
    {
        return await _crypto.DecryptStream(await base.ReadFile(path));
    }

    public override async Task WriteFile(string path, Func<Stream, Task> write)
    {
        await base.WriteFile(path, async stream =>
        {
            using (var encrypted = await _crypto.Encrypt(async cryptoStream => await write(cryptoStream)))
            {
                await encrypted.CopyToAsync(stream);
            }
        });
    }

    public override Task AppendFile(string path, Func<Stream, Task> write)
    {
        throw new NotSupportedException("You cannot append to an encrypted file.  Try reading, and the writing the whole file.");
    }
}

Testing

The easiest way of testing code using an IFileSystem dependency is to use the InMemoryFileSystem, which will behave the same as the physical file system.

var fileSystem = new InMemoryFileSystem();

var sut = new TestClass(fileSystem);
sut.Execute();

fileSystem
    .ReadLines("./the/file.txt")
    .ShouldBe(new[] { "first", "second", "third" });

Alternatly, if you want to assert on something written to a stream, e.g. on a .AppendFile() call, you can do it manually (this example using NSubstitute):

var ms = new MemoryStream();
var fileSystem = Substitute.For<IFileSystem>();
fileSystem
    .AppendFile("wat", Arg.Do<Func<Stream, Task>>(func => func(ms).Wait()))
    .Returns(Task.CompletedTask);

fileSystem.AppendFile("wat", async stream => {
    await stream.WriteAsync(new byte[] { 1, 2, 3 }, 0, 3);
});

ms.ToArray().ShouldBe(new byte[] { 1, 2, 3});

Or if you want to capture many streams, you can use the provided StreamCapture class:

var streams = new StreamCapture();
var fileSystem = Substitute.For<IFileSystem>();
fileSystem
    .AppendFile("wat", Arg.Do<Func<Stream, Task>>(streams.Capture))
    .Returns(Task.CompletedTask);

fileSystem.AppendFile("wat", async stream => {
    await stream.WriteAsync(new byte[] { 1, 2, 3 }, 0, 3);
});

streams.Last.ToArray().ShouldBe(new byte[] { 1, 2, 3});

To do

  • Caching FileSystem
    • Read caching I guess
    • pluggable caching strategies
  • Commitable FileSystem (call .Commit() to flush writes to disk.)
  • Case(In)Sensitive FileSystem?
    • Not sure how this would work
    • ReadFile would ListFiles first, then find the right mapping perhaps?
  • S3FileSystem
    • Separate package I guess

  • .NETFramework 4.5.1

    • No dependencies.
  • .NETFramework 4.6.1

    • No dependencies.
  • .NETStandard 2.0

    • No dependencies.

Version History

Version Downloads Last updated
0.5.1 451 7/12/2018
0.5.0 187 7/10/2018
0.4.0 324 10/19/2017
0.3.0 263 9/25/2017
0.2.2 275 4/28/2017
0.2.1 210 4/26/2017
0.2.0 212 4/15/2017
0.1.0 204 4/7/2017