FileBasedApp.Toolkit
1.3.0
dotnet add package FileBasedApp.Toolkit --version 1.3.0
NuGet\Install-Package FileBasedApp.Toolkit -Version 1.3.0
<PackageReference Include="FileBasedApp.Toolkit" Version="1.3.0" />
<PackageVersion Include="FileBasedApp.Toolkit" Version="1.3.0" />
<PackageReference Include="FileBasedApp.Toolkit" />
paket add FileBasedApp.Toolkit --version 1.3.0
#r "nuget: FileBasedApp.Toolkit, 1.3.0"
#:package FileBasedApp.Toolkit@1.3.0
#addin nuget:?package=FileBasedApp.Toolkit&version=1.3.0
#tool nuget:?package=FileBasedApp.Toolkit&version=1.3.0
FileBasedApp.Toolkit
A collection of opinionated helpers and extensions for building file-based applications in .NET.
Features
- Provides a
PathUtilclass for working with paths and validating string paths - Provides
LocalPathExtensionswithIsHome,HasHomeDirectoryPrefix(), andExpandHomeDirectory()for home-directory expansion,IsCurrentDirectory()for lexical current-directory detection, plusGetSegments()andTraverse()for lexical path traversal - Provides an IO class to provide methods and extension methods for
TruePathFindInFiles— filter files by stream predicate or regex (withByLine/AllTextstrategies)Replace— in-place file text transformationSafeDeleteDirectory— safely delete directories with optional exception handling
- Provides
StringExtensionswithStringJoin,IsNullOrWhitespace, andIsNullOrEmptyconvenience methods - Provides an extension of the
CommandSettingsfrom theSpectre.Console.Cliproviding validation of file and directory paths - Provides a
SimpleExecRunnerfluent builder for constructing and executing system commands via theSimpleExeclibrary, with support for secrets redaction andTruePathintegration - Includes the following libraries
TruePath2.0.0SimpleExecSpectre.Console.CliTestableIO.System.IO.Abstractions.WrappersTruePath.TestableIO.System.IO1.1.0
Example
Extended settings
The example below shows how to use the ExtendedCommandSettings class to validate file and directory paths.
using FileBasedApp.Toolkit;
using Spectre.Console;
using Spectre.Console.Cli;
using TruePath;
public class CustomCommandSettings : ExtendedCommandSettings
{
[CommandArgument(0, "<directory-path>")]
public string? Directory { get; set; }
[CommandOption("--filePath")]
public required string File { get; set; }
public AbsolutePath DirectoryPath { get; set; }
public AbsolutePath FilePath { get; set; }
protected override ValidationResult DoValidate()
{
// Evaluates the directory string.
DirectoryPath = TryGetDirectory(Directory,
allowEmpty: true,
shouldExist: true,
PredefinedRootPath.ExecutionFolder);
FilePath = TryGetFile(File, true, roots: [PathUtil.GetCurrentWorkingFolder(), PathUtil.GetExecutionFolder()]);
return base.DoValidate();
}
}
TruePath IO extensions
using System.IO.Abstractions;
using FileBasedApp.Toolkit;
var applicationData = Environment.SpecialFolder.ApplicationData.GetSpecialFolder();
applicationData.FindRequiredParent(x => x.FileName == "SomeValue");
var ancestors = applicationData.GetAncestors(true).ToList();
// Combining with TruePath.TestableIO.System.IO
// This was orignally part of the FileBased.Toolkit library but moved
// to it's own library
var newDirectory = applicationData / "NewDirectory";
newDirectory.CreateDirectory();
IFileSystem fileSystem = new FileSystem();
fileSystem.File.Create(newDirectory / "test.txt");
(newDirectory / "..").GetDirectories(fileSystem)
Home-directory expansion
HasHomeDirectoryPrefix() detects a leading ~ segment in a relative path. ExpandHomeDirectory() replaces that segment with the user profile directory, preserving the remaining segments. Absolute paths and relative paths without a home-directory prefix are returned unchanged; this does not convert arbitrary relative paths to absolute paths.
using FileBasedApp.Toolkit;
using TruePath;
var path = new LocalPath("~/Documents/file.txt");
var hasHomePrefix = path.HasHomeDirectoryPrefix(); // true
var expanded = path.ExpandHomeDirectory(); // User profile directory / Documents / file.txt
IsHome identifies only the ~ marker itself. GetSegments() enumerates segments root-first, while Traverse() enumerates them leaf-first.
Current-directory paths and traversal
IsCurrentDirectory() checks the normalized path lexically: empty input, . and a/.. all represent the current-directory marker. An absolute path is not a marker, even when it points to the current working directory. This check does not access the file system.
using FileBasedApp.Toolkit;
using TruePath;
var path = new LocalPath("a/..");
var isCurrentDirectory = path.IsCurrentDirectory(); // true; Value is "." in TruePath 2.0
var segments = path.GetSegments(); // No segments; Traverse() is also empty
Traversal preserves absolute roots, Windows roots without a drive, and relative parent markers (..). A rooted-without-drive path such as \foo\bar has segments \, foo, and bar. Drive-relative prefixes remain attached to the first segment: C:foo\bar has segments C:foo and bar, while C: remains a single segment. Combining these segments in root-first order with / reconstructs the normalized path without resolving its drive or current-directory dependencies. Windows-only regression tests assert these segment and reconstruction contracts; cross-platform test execution remains unverified.
TruePath 2.0 migration notes
The toolkit now references TruePath 2.0.0 and the TestableIO bridge 1.1.0. TruePath normalizes empty input and paths such as a/.. to .; the toolkit still returns no traversal segments for these paths.
On Windows, PathUtil.AnalyzeFile() and AnalyzeDirectory() resolve inputs against a supplied absolute root as follows (root: C:\base):
| Input | Resolved path |
|---|---|
\logs |
C:\logs |
C:logs |
C:\base\logs |
C: |
C:\base |
D:logs |
Resolved against the process's current directory for drive D |
D:\logs |
D:\logs |
Drive-relative inputs are currently accepted. Cross-drive relative resolution depends on process/per-drive state, not just the supplied root; use fully qualified paths when deterministic resolution is required. Windows-only regression tests cover these cases for both file and directory analysis.
Absolute-path conversions require fully qualified input
AsAbsolutePath() (string and character-span overloads) and AsRequiredAbsolutePath() validate
fully qualified paths; they do not resolve relative input against the current directory.
On Windows, C:\foo is valid, but \foo (unspecified drive), C:foo, and C:
(relative to a drive's current directory) throw ArgumentException under TruePath 2.0.
On Unix, use an absolute path such as /foo. Empty and relative input also throw.
The nullable-string AsAbsolutePath() returns null only for null input, not for an invalid
path. AsRequiredAbsolutePath() throws ArgumentNullException for null. The span overload
throws for invalid or empty input rather than returning null.
Use PathUtil.AnalyzeFile() or AnalyzeDirectory() when resolution against a supplied root
is intended, or keep a relative path as a LocalPath instead of converting it.
Breaking change in the next minor release: FlexibleFilePath accepts relative paths
This behavioral breaking change will ship in a minor release, not a major release.
FlexibleFilePath.GetAsLocalPath() and its implicit conversion to LocalPath now construct
a LocalPath directly, selecting Windows on Windows and Unix elsewhere. Previously they
constructed an AbsolutePath, rejecting ordinary relative input.
Relative paths such as folder/file.txt are now accepted and remain relative; they are not
resolved against the current directory. Empty input and . represent the current-directory
marker. Windows inputs such as \foo, C:foo, and C: are also accepted without resolving
their drive or current-directory dependencies. Fully qualified paths remain supported.
Migration: Do not assume the returned LocalPath is absolute or rely on conversion throwing
for relative input. If an absolute path is required, validate it explicitly with
AbsolutePath.Create(localPath.Value) (which rejects non-fully-qualified input), or use
PathUtil.AnalyzeFile() / AnalyzeDirectory() to resolve it against a supplied root.
var configured = new FlexibleFilePath(@"data\file.txt", "data/file.txt");
LocalPath path = configured; // Relative on either platform; no longer throws
SimpleExecRunner
The SimpleExecRunner provides a fluent builder API on top of the SimpleExec library for constructing and executing system commands. It supports TruePath types (AbsolutePath, LocalPath) directly as arguments, and can redact secrets from echoed output.
using FileBasedApp.Toolkit.SimpleExec;
using Spectre.Console;
using TruePath;
// Basic command execution
await new SimpleExecRunner("dotnet")
.AddArgument("build")
.AddArgument("MyProject.csproj")
.RunAsync();
// Using AddArgumentPair for flag + value combinations
await new SimpleExecRunner("dotnet")
.AddArgument("pack")
.AddArgumentPair("-c", "Release")
.AddArgumentPair("-o", outputPath) // accepts AbsolutePath directly
.RunAsync();
// With secrets redaction — the API key value is replaced with "***" in echoed output
await new SimpleExecRunner("dotnet")
.AddArguments("nuget", "push")
.AddArgument(packagePath)
.AddArgumentPair("--source", "nuget.org")
.AddArgumentPair("--api-key", apiKey, isSecret: true)
.RunAsync();
// Reading command output
var (stdout, stderr) = await new SimpleExecRunner("dotnet")
.AddArgument("--version")
.ReadAsync();
// Working directory and environment variables
await new SimpleExecRunner("git")
.AddArguments("status", "--porcelain")
.WithWorkingDirectory(repoRoot)
.WithConfigureEnvironment(env => env["GIT_TERMINAL_PROMPT"] = "0")
.RunAsync();
// Accept an expected non-zero exit code. `git diff --quiet` returns 1 when
// differences exist, which is an expected result rather than a command failure.
await new SimpleExecRunner("git")
.AddArguments("diff", "--quiet")
.WithAcceptedErrorCodes([1])
.RunAsync();
Exit code 0 remains successful by default and does not need to be listed. Other non-zero exit codes still cause the command to throw. Use WithExitCodeHandler when you need the lower-level handled-exit-code callback instead of a fixed list of accepted codes.
Use HandlesAllErrorCodes() only when every exit code is an expected result; it disables SimpleExec's default non-zero-exit-code failure behavior.
Use the result-returning APIs when the observed exit code or captured output is needed after execution:
var result = await new SimpleExecRunner("git")
.AddArguments("diff", "--quiet")
.WithAcceptedErrorCodes([1])
.ReadEnhancedAsync();
AnsiConsole.WriteLine(result.ExitCode.ToString());
AnsiConsole.WriteLine(result.StandardOutput);
AnsiConsole.WriteLine(result.StandardError);
RunWithExitCode() and RunWithExitCodeAsync() return SimpleExecRunResult, which contains ExitCode. ReadEnhancedAsync() returns SimpleExecReadResult, which contains ExitCode, StandardOutput, and StandardError. These methods still honor the configured exit-code policy.
Key types:
SimpleExecRunner— the fluent builder. Create withnew SimpleExecRunner("command-name"), add arguments, then callRun(),RunAsync(),ReadAsync(), or a result-returning execution method.SimpleExecRunResult/SimpleExecReadResult— result types returned by the exit-code-aware execution methods.ISimpleExecCommandWrapper— an interface wrappingSimpleExec.Commandto enable unit testing. The default implementation (SimpleExecCommand) delegates directly to the staticCommandclass.
Web URIs (AbsoluteWebUri / RelativeWebUri)
Strongly-typed, immutable wrappers around System.Uri for composing web URIs. Path segments, query strings, and fragments are represented as validated value objects (UriPathSegment, UriQueryString, UriFragment) and can be combined fluently via the / operator.
using FileBasedApp.Toolkit;
using Spectre.Console;
// Build an absolute URI fluently
var url = AbsoluteWebUri.Create("https://example.com")
/ UriPathSegment.From("first")
/ UriPathSegment.From("second")
/ UriQueryString.From("a=1&b=2")
/ UriFragment.From("Fragment");
// https://example.com/first/second?a=1&b=2#Fragment
AnsiConsole.WriteLine(url.ToString());
// Or use method form
var api = AbsoluteWebUri.Create("https://example.com/api")
.AddPathSegment(UriPathSegment.From("users"))
.AddQueryPart("page", "1")
.WithFragment(UriFragment.From("top"));
// Replace the entire path with a single segment (preserves query/fragment)
var reset = AbsoluteWebUri.Create("https://example.com/old/path?q=1")
.WithPathSegment(UriPathSegment.From("new"));
// https://example.com/new?q=1
// Relative URIs work the same way
var relative = RelativeWebUri.Create("/search")
/ UriQueryString.From("q=hello");
Each operation returns a new instance, so the originals are never mutated. Invalid inputs (empty segments, illegal characters) throw at the value-object construction step via Vogen validation.
HTTP extensions
AbsoluteWebUriHttpExtensions adds a WithBaseAddress extension on HttpClient that accepts an IWebUri<TSelf>, and HttpResponseMessageExtensions adds ToJson<T> / ToRequiredJson<T> for deserializing response content via a source-generated JsonTypeInfo<T>. Both deserialization helpers call EnsureSuccessStatusCode first; ToRequiredJson additionally throws InvalidOperationException when deserialization yields null.
using System.Text.Json.Serialization;
using FileBasedApp.Toolkit;
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Widget))]
internal partial class AppJsonContext : JsonSerializerContext { }
public record Widget(string Name, int Count);
var baseUri = AbsoluteWebUri.Create("https://example.com/api");
var httpClient = new HttpClient().WithBaseAddress(baseUri);
using var response = await httpClient.GetAsync("widgets?id=1");
var widget = await response.ToRequiredJson(AppJsonContext.Default.Widget);
Template
You can use the FileBasedApp.Toolkit.Template to easily create a new filebase app with FileBasedApp.Toolkit references added https://www.nuget.org/packages/FileBasedApp.Toolkit.Template/
Bugs or things missing
Feel free to create an issue or submit a pull request.
Credits
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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 was computed. 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. |
-
net10.0
- JetBrains.Annotations (>= 2026.2.0)
- NuGet.Versioning (>= 7.9.0)
- SimpleExec (>= 13.1.0)
- Spectre.Console.Cli (>= 0.57.2)
- TestableIO.System.IO.Abstractions.Wrappers (>= 22.3.0)
- TruePath (>= 2.0.0)
- TruePath.TestableIO.System.IO (>= 1.1.0)
- Vogen (>= 8.0.7)
-
net8.0
- JetBrains.Annotations (>= 2026.2.0)
- NuGet.Versioning (>= 7.9.0)
- SimpleExec (>= 13.1.0)
- Spectre.Console.Cli (>= 0.57.2)
- TestableIO.System.IO.Abstractions.Wrappers (>= 22.3.0)
- TruePath (>= 2.0.0)
- TruePath.TestableIO.System.IO (>= 1.1.0)
- Vogen (>= 8.0.7)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on FileBasedApp.Toolkit:
| Package | Downloads |
|---|---|
|
FileBasedApp.Toolkit.CSharp
Roslyn-based helpers for detecting and working with file-based .NET apps. |
|
|
FileBasedApp.Toolkit.Dotnet
Fluent builders and recipes for dotnet CLI commands (pack, nuget push, package search). |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 1.3.0 | 0 | 11/8/2026 | |
| 1.2.0 | 169 | 10/12/2026 | |
| 1.1.2 | 197 | 9/15/2026 | |
| 1.1.1 | 244 | 8/29/2026 | |
| 1.1.0 | 153 | 8/29/2026 | |
| 1.0.1 | 178 | 8/25/2026 | |
| 1.0.0 | 170 | 8/23/2026 | |
| 0.21.0 | 431 | 7/12/2026 | |
| 0.20.0 | 256 | 4/17/2026 | |
| 0.20.0-rc-01 | 158 | 4/16/2026 | |
| 0.19.1 | 149 | 4/8/2026 | |
| 0.19.0 | 184 | 4/8/2026 | |
| 0.19.0-rc-01 | 159 | 4/8/2026 | |
| 0.18.0 | 193 | 4/5/2026 | |
| 0.18.0-rc-01 | 148 | 4/5/2026 | |
| 0.17.0 | 180 | 3/29/2026 | |
| 0.17.0-rc-01 | 147 | 3/25/2026 | |
| 0.16.0 | 190 | 3/15/2026 | |
| 0.15.0 | 179 | 3/13/2026 |
- Updated TruePath to 2.0.0 and TruePath.TestableIO.System.IO to 1.1.0, including changed Windows path resolution and stricter absolute-path validation.
- Added IsCurrentDirectory() for lexical current-directory detection.
- Preserved zero-segment traversal for empty, dot, and normalized current-directory paths.
- Added Windows-specific file and directory path-resolution regression coverage.