StrictPath.Core
0.1.1
dotnet add package StrictPath.Core --version 0.1.1
NuGet\Install-Package StrictPath.Core -Version 0.1.1
<PackageReference Include="StrictPath.Core" Version="0.1.1" />
<PackageVersion Include="StrictPath.Core" Version="0.1.1" />
<PackageReference Include="StrictPath.Core" />
paket add StrictPath.Core --version 0.1.1
#r "nuget: StrictPath.Core, 0.1.1"
#:package StrictPath.Core@0.1.1
#addin nuget:?package=StrictPath.Core&version=0.1.1
#tool nuget:?package=StrictPath.Core&version=0.1.1
StrictPath
An immutable, strict path/file API for .NET 10.
One class — SPath — wraps an absolute path and makes risky operations explicit:
writes never overwrite existing files unless you pass overwrite: true, and
the guarantee is enforced atomically at the OS level (FileMode.CreateNew),
not by a check-then-write race.
Works on Windows, macOS, and Linux (see Cross-platform behavior).
This is a monorepo with three packages:
| Project | What it is |
|---|---|
StrictPath.Core |
TheSPath type: path algebra, assertions, strict I/O (sync + async, System.Text.Json). |
StrictPath.Json |
ReadNewtonsoftJson / WriteNewtonsoftJson extensions (Newtonsoft.Json). |
StrictPath.Zip |
ZipFrom / Unzip extensions (SharpZipLib) with portable entry names and zip-slip protection. |
Quick start
using StrictPath;
var root = new SPath("data").Create(); // mkdir -p data
var file = root / "config.json"; // join with '/'
file.WriteJson(new { Port = 8080 }, indent: true); // throws if file exists
file.WriteJson(new { Port = 9090 }, overwrite: true); // explicit overwrite
var cfg = file.ReadJson<Dictionary<string, int>>(); // System.Text.Json
await file.WriteTextAsync("...", overwrite: true); // async variants throughout
var backup = root.Join("backup").Create();
var copied = file.CopyIn(backup); // backup/config.json
copied.MoveTo(backup / "old.json");
foreach (var f in root.FilesDeep()) // recursive file listing
Console.WriteLine(f.RelativeTo(root));
Design principles
- Immutable.
SPathis a value: every transformation (Join,WithExtension,Parent,ToReal) returns a new instance. Instances are safe to share and compare (IEquatable<SPath>,==, hashable). - Strict by construction. Every write opens the file with
FileMode.CreateNewunlessoverwrite: trueis passed, so "never clobber by accident" holds even against concurrent writers. - Destruction is named at the call site. The only ways to lose data are
Delete(),overwrite: true, andExistingFile.Overwrite— there is no hidden overwrite state to arm or forget, and no convenience method that quietly bundles a delete. - One exception type. Every violated expectation throws
StrictPathExceptionwith a message naming the path. It derives fromIOException, so existingcatch (IOException)handlers still work. - Properties are cheap, methods do work.
Exists/IsFile/IsDirectoryare properties; anything that reads file contents or walks directories is a method (GetSize(),GetCrc32(),GetRealPath(),Files(), …).
API overview
Path algebra (pure, no disk access)
| Member | Meaning |
|---|---|
new SPath(path) |
Normalizes to an absolute path (Path.GetFullPath). |
Join(...) / a / "sub" |
Combine paths. |
WithExtension(".txt") |
Replace the extension (""/null removes it; must start with .). |
AppendExtension(".zip") |
file.json → file.json.zip |
Name, Stem, Extension, Parent |
dir/file.json → file.json, file, .json, dir |
RelativeTo(ancestor) |
Relative path string fromancestor. |
ToString() |
ReturnsFullPath; never touches the disk. |
Queries
IsFile, IsDirectory, Exists, Optional() — existence.
GetSize(), GetCrc32() — file facts.
Children(), Files(), Directories(), FilesDeep(), Glob(pattern) —
enumeration; all require the path to be a directory and throw otherwise.
Glob matches files only.
GetRealPath() / ToReal() — the exact on-disk casing (see below).
Assertions
AssertFile(), AssertDirectory(), AssertExists(), AssertNotExists(),
AssertExtension(".json"), AssertDescendantOf(dir), AssertSubpathOf(dir).
All throw StrictPathException on failure and return this for chaining.
The containment assertions are purely lexical (no disk access, symlinks not
resolved).
I/O — strict by default
ReadText, ReadBytes, ReadJson<T>, OpenText and
WriteText, WriteBytes, WriteJson, OpenWrite — each write takes
overwrite: false by default; each has an Async variant (except Open*,
whose streams support async I/O directly).
file.WriteText("v1"); // ok, file is new
file.WriteText("v2"); // StrictPathException
file.WriteText("v2", overwrite: true); // ok — explicit, this call only
file.Delete().CreateParents().WriteText("v3"); // destruction spelled out, by name
Operations
Create(), CreateParents(), CreateTmp(parent?), CreateAsJson(),
Delete(recursive?) (idempotent),
MoveTo / MoveFrom, CopyIn / CopyContents / CopyFrom — all take
overwrite: false by default; a move never replaces an existing directory.
Zip (StrictPath.Zip)
using StrictPath.Zip;
var zip = new SPath("data.zip").ZipFrom(folder); // zip a folder
zip.Unzip(); // throws on existing files (default)
zip.Unzip(target, ExistingFile.Skip); // or Skip / Overwrite
- Entry names always use
/(zip spec), so Windows-created archives extract correctly everywhere. - Extraction rejects zip slip (entries with
..or absolute names) withStrictPathException, and normalizes\entries from legacy Windows tools. - Only files are stored; empty directories produce no entries.
Newtonsoft JSON (StrictPath.Json)
using StrictPath.Json;
file.WriteNewtonsoftJson(obj, indent: true); // strict, like every write
var value = file.ReadNewtonsoftJson<MyType>();
Cross-platform behavior
- Separators —
/and\both accepted on Windows; produced paths use the platform separator. - Equality —
SPath.PathComparison: ordinal case-insensitive on Windows/macOS, ordinal case-sensitive on Linux, matching each platform's default file-system semantics. GetRealPath()— returns the exact on-disk casing; handles drive roots (C:\), UNC shares (\\server\share), and the Unix root (/). When bothfooandFOOexist (case-sensitive file systems), each resolves to itself.- Extensions —
AssertExtensionis case-insensitive (.json==.JSON) on every platform. CreateTmp()— creates a randomly-namedtmp_xxxxxxxxdirectory under the current working directory by default (not the system temp dir); random names keep concurrent callers from colliding.- CI — .github/workflows/ci.yml runs the suite on Linux, Windows, and macOS; the case-sensitivity tests only execute on Linux.
Building and testing
dotnet test StrictPath.slnx # requires the .NET 10 SDK
Publishing to nuget.org
Pushing a version tag packs all three packages with that version and publishes
them via the publish job in ci.yml:
git tag v1.0.0 && git push origin v1.0.0
Publishing uses nuget.org Trusted Publishing (OIDC) — no stored API key.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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
- Microsoft.Extensions.FileSystemGlobbing (>= 8.0.0)
- System.IO.Hashing (>= 8.0.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on StrictPath.Core:
| Package | Downloads |
|---|---|
|
StrictPath.Zip
Zip/unzip extension methods for StrictPath's SPath, with portable entry names and zip-slip protection. |
|
|
StrictPath.Json
Newtonsoft.Json extension methods for StrictPath's SPath. |
GitHub repositories
This package is not used by any popular GitHub repositories.