A strongly-typed file system abstraction for .NET that layers build-aware path resolution and reversible file
transformations on top of System.IO.Abstractions.
Stop hard-coding paths and scattering magic strings across your build and tooling code: resolve well-known locations
by key, compose paths fluently, and make temporary, automatically-rolled-back edits to files.
- Rooted paths.
RootedPathcarries its ownIFileSystem, so paths and I/O always travel together, and composes fluently with the/operator. - Keyed path resolution. Resolve locations such as
"Root"or"Artifacts"through a prioritized chain ofIPathProviderimplementations, with results cached after the first lookup. - Strongly-typed path markers. Give compile-time names to well-known paths via
IPathMarkerinstead of relying on magic strings. - Reversible file transforms.
TransformFileScopeandTransformMultiFileScopeapply temporary edits to one or many files and restore the originals on dispose, or commit them withCancelRestore(). - Dependency-injection first. A single
AddRootedFileSystem()call wires everything up; register custom providers withProvidePath(...). - Testable. Everything works against
IFileSystem, so aMockFileSystemdrops straight into your tests.
dotnet add package Invex.FileSystemThe package targets .NET 10.0. It depends on System.IO.Abstractions,
Microsoft.Extensions.DependencyInjection.Abstractions and Microsoft.Extensions.Hosting.Abstractions. The rooted file
system logs path resolutions through ILogger, so register logging (for example services.AddLogging()) in your
container. Generic-host apps already have both; a standalone console or build tool also needs the
Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.Logging packages for BuildServiceProvider() and
AddLogging():
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.LoggingRegister the file system with your DI container and declare where your well-known paths live:
using Invex.FileSystem;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddLogging();
// Register the rooted file system.
services.AddRootedFileSystem();
// Describe where your well-known paths live.
services.ProvidePath((key, fs) => key switch
{
"Root" => fs.CurrentDirectory,
"Artifacts" => fs.GetPath("Root") / "artifacts",
_ => null,
});
using var provider = services.BuildServiceProvider();
var fs = provider.GetRequiredService<IRootedFileSystem>();
// Resolve and compose paths.
RootedPath artifacts = fs.GetPath("Artifacts");
RootedPath logFile = artifacts / "logs" / "build.log";
if (!logFile.PathExists)
fs.Directory.CreateDirectory(artifacts);A RootedPath binds an absolute path string to the IFileSystem it belongs to, so existence checks and queries need
no extra context. It implicitly converts to string for any API that expects one:
RootedPath path = fs.GetPath("Root") / "src" / "Project.csproj";
bool exists = path.PathExists; // file or directory
RootedPath? parent = path.Parent; // null at the root
File.ReadAllText(path);See Rooted Paths.
IRootedFileSystem.GetPath(string) queries every registered IPathProvider in descending priority order and caches
the first non-null result. Use the (key, fs) overload of ProvidePath when a path depends on another resolved path:
services.ProvidePath((key, fs) => key == "Output"
? fs.GetPath("Root") / "output"
: null, priority: 5);See Path Providers.
Implement IPathMarker when a path is statically determined by a type. Markers bypass the provider chain and cache:
public sealed class ArtifactsPath : IPathMarker
{
public static RootedPath Path(IFileSystem fileSystem) =>
((IRootedFileSystem)fileSystem).GetPath("Root") / "artifacts";
}
RootedPath artifacts = fs.GetPath<ArtifactsPath>();See Path Markers.
TransformFileScope captures a file's content, applies a transform and restores the original when the scope is
disposed. TransformMultiFileScope does the same for a set of files:
await using (var scope = await TransformFileScope.CreateAsync(
fs.GetPath("Root") / "Directory.Build.props",
content => content.Replace("1.0.0", "2.0.0")))
{
// The file now contains "2.0.0"; run your build step here.
}
// On dispose the original content is restored automatically.Construct a RootedPath over a MockFileSystem, or override the keyed "RootedFileSystem" registration in your
container:
var mock = new MockFileSystem();
var path = new RootedPath(mock, "/repo/file.txt");
mock.AddFile(path, new MockFileData("hello"));
// path.FileExists is now trueSee Testing.
Full documentation: https://invex-games.github.io/filesystem/
| Guide | Contents |
|---|---|
| Introduction | What the library solves, when to use it, and the pieces at a glance. |
| Getting Started | Installation, DI registration, and your first resolved paths. |
| Rooted Paths | Creating, composing and querying RootedPath. |
| Path Providers | Priorities, caching, custom providers and error handling. |
| Path Markers | Strongly-typed paths with IPathMarker. |
| File Transform Scopes | Temporary, reversible single- and multi-file edits. |
| Testing | Using MockFileSystem with rooted paths and transform scopes. |
| API Reference | Every public type, with links into the generated reference. |
dotnet build Invex.FileSystem.slnx
dotnet test Invex.FileSystem.slnxThe documentation site is built with DocFX through the
Atom build in _atom/:
atom BuildDocs # generate the site
atom ServeDocs # build and serve it locallyWithout the atom tool installed, run the same targets with dotnet run --project _atom -- BuildDocs.
Licensed under the MIT License.