Use a custom File System abstraction
Context and Problem Statement
The APIs of the standard library (System.IO
) use the real file system and basic types like string
for paths and
integers for file sizes. Using the real file system not only slows down test execution, but it can also have unforeseen
consequences when running tests in parallel.
Another issue arises when trying to support tools like Wine
and Proton. Wine is a compatability layer that can run Windows applications
on Linux, without requiring a virtual machine. Wine creates "prefixes", which contain a virtual C:
drive. Using the
APIs of the standard library, we are unable to correctly make use of this virtual drive, without rewriting all internal
and external code, to be aware of Wine prefixes, and other virtualization.
Decision Drivers
We need a library with the following features:
- provides an abstraction over the real file system
- provides a mock-able interface for testing
- provides an implementation that uses the real file system
- provides an implementation that uses an in-memory file system for testing
- exposes a limited set of public APIs which uses nominal typing
- allows for path re-mapping:
/foo/bar
should redirect to/baz
- allows for cross-platform path re-mapping:
C:\\foo\\bar
should be converted to/c/foo/bar
and redirected to/baz
Considered Options
-
System.IO.Abstraction
: this provides fully mock-able interfaces and test implementations for the entireSystem.IO
namespace. As such, it contains almost every function from the standard library, but returns interfaces wherever possible. The library doesn't change any functionality, nor any function signature, aside from returning it's own interface types, when necessary. Finally, this library doesn't allow for path re-mappings, doesn't properly work with cross-platform paths, as it just redirects all calls to the standard library. -
Custom library: the DIY approach allows us to provide a limited set of APIs, tailored towards our use-case that also uses our internal types.
Decision Outcome
We created a custom interface called IFileSystem
in the NexusMods.Paths
project, with an implementation that uses
the real file system called FileSystem
, and an implementation that uses an in-memory fake file system
called InMemoryFileSystem
.
The available APIs in IFileSystem
use our custom types:
AbsolutePath
instead of raw stringsSize
instead of raw integersIFileEntry
instead ofSystem.IO.FileInfo
IDirectoryEntry
instead ofSystem.IO.DirectoryInfo
Path re-mapping is done in the abstract class BaseFileSystem
, which implements IFileSystem
and is the parent class
of FileSystem
and InMemoryFileSystem
. Having this parent class reduces code duplication and provides separation of
concerns for the concrete implementations:
flowchart TD
A[IFileSystem.OpenFile] -- path = /foo/bar --> B[BaseFileSystem.OpenFile]
B -- path = /foo/bar --> C[BaseFileSystem.GetMappedPath]
C -- path = /foo/baz --> D[FileSystem.InternalOpenFile]
D --> E[System.IO.File.Open]
Consequences
The app is very IO heavy by design, and thus has a lot of file system calls. Replacing all mentions of System.IO
with
the new file system abstraction will take some time. Internal legacy APIs have been marked as deprecated and new code
should make use of IFileSystem
without any issue.