Skip to main content

use-fullpath

Ensure .NET projects use `Meziantou.Framework.FullPath` for local file path manipulation instead of raw strings. Use when: reviewing or writing code that constructs, combines, compares, or passes around local file/directory paths as strings in .NET/C# projects.

Source facts

Repository
meziantou/Meziantou.Framework
Last source activity
May 4, 2026 at 03:33
Detected SKILL.md language
English
Stars
868
Forks
113

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
use-fullpath
description
Ensure .NET projects use `Meziantou.Framework.FullPath` for local file path manipulation instead of raw strings. Use when: reviewing or writing code that constructs, combines, compares, or passes around local file/directory paths as strings in .NET/C# projects.
# Use FullPath for Local Path Manipulation Use this skill when reviewing or writing .NET code that manipulates local file or directory paths. ## Why Raw `string` paths are a common source of bugs: - A path can be absolute or relative (`c:\a\b` vs `.\a\b`) - A path that looks absolute can actually be relative (`c:a` is relative) - Paths may contain `.` or `..` segments that aren't resolved - Trailing directory separators cause inconsistent comparisons (`c:\a` vs `c:\a\`) - Path comparison must be case-insensitive on Windows and case-sensitive on Linux - Reserved device names on Windows (`CON`, `PRN`, `NUL`) can cause subtle failures - When a `FileNotFoundException` occurs, relative paths make debugging difficult - A `string` parameter gives no signal whether a full path or a relative path is expected The `FullPath` struct from `Meziantou.Framework.FullPath` solves these problems by guaranteeing the stored value is always a fully-resolved absolute path with normalized separators. ## Installation ```shell # .NET 10+ dotnet package add Meziantou.Framework.FullPath # .NET 9 and earlier dotnet add package Meziantou.Framework.FullPath ``` The namespace is `Meziantou.Framework`. ## Core Patterns ### Creating a FullPath ```csharp // From a string (relative paths are resolved against the current directory) FullPath path = FullPath.FromPath("demo"); // Well-known locations FullPath temp = FullPath.GetTempPath(); FullPath cwd = FullPath.CurrentDirectory(); FullPath docs = FullPath.GetFolderPath(Environment.SpecialFolder.MyDocuments); ``` ### Combining Paths Use the `/` operator or `FullPath.Combine` — never `string.Concat` or string interpolation. ```csharp FullPath root = FullPath.FromPath("/repo"); // / operator (preferred for readability) FullPath file = root / "src" / "Program.cs"; // Combine (useful when the number of segments is dynamic) FullPath file2 = FullPath.Combine(root, "src", "Program.cs"); ``` ### Comparing Paths Comparisons are case-insensitive on Windows/macOS and case-sensitive on Linux by default. Navigation segments and trailing separators are already resolved, so equality just works. ```csharp // == and != use the OS-default comparison if (pathA == pathB) { /* same file */ } // Explicit case-sensitivity control bool equal = pathA.Equals(pathB, ignoreCase: false); ``` ### Navigating the Path Tree ```csharp FullPath file = FullPath.FromPath("/repo/src/Program.cs"); FullPath dir = file.Parent; // /repo/src string name = file.Name; // Program.cs string nameNoExt = file.NameWithoutExtension; // Program string ext = file.Extension; // .cs FullPath renamed = file.ChangeExtension(".vb"); // /repo/src/Program.vb ``` ### Checking Hierarchy ```csharp FullPath root = FullPath.FromPath("/repo"); FullPath file = root / "src" / "Program.cs"; bool isChild = file.IsChildOf(root); // true ``` ### Making a Relative Path ```csharp FullPath root = FullPath.FromPath("/repo"); FullPath file = root / "src" / "Program.cs"; string relative = file.MakePathRelativeTo(root); // src/Program.cs (or src\Program.cs on Windows) ``` ### Walking Up the Tree ```csharp FullPath start = FullPath.FromPath("/repo/src/deep/nested"); // Find the first ancestor (or self) matching a predicate if (start.TryFindFirstAncestorOrSelf( p => File.Exists(p / ".editorconfig"), out FullPath match)) { // match is the closest directory containing .editorconfig } // Shortcut: find the Git repository root if (start.TryFindGitRepositoryRoot(out FullPath gitRoot)) { // gitRoot is the closest directory containing .git } ``` ### Interop with System.IO `FullPath` has an implicit conversion to `string`, so it works directly with `File`, `Directory`, and most APIs that accept a string path. ```csharp FullPath config = root / "appsettings.json"; // No .Value or .ToString() needed string json = File.ReadAllText(config); File.WriteAllText(config, json); Directory.CreateDirectory(config.Parent); ``` When you need the raw string explicitly (e.g., for logging or interpolation): ```csharp logger.LogInformation("Loading config from {Path}", config.Value); ``` ### Ensuring the Parent Directory Exists ```csharp FullPath output = root / "artifacts" / "report.html"; output.CreateParentDirectory(); // creates "artifacts" if it doesn't exist File.WriteAllText(output, html); ``` ### JSON Serialization `FullPath` has a built-in `System.Text.Json` converter. It serializes to a plain JSON string and deserializes back through `FullPath.FromPath`. ```csharp public sealed class AppSettings { public FullPath OutputDirectory { get; set; } } // { "OutputDirectory": "C:\\repo\\output" } var settings = JsonSerializer.Deserialize<AppSettings>(json); ``` ### Using FullPath in Method Signatures Prefer `FullPath` over `string` in APIs that expect an absolute path. This makes intent explicit at the type level. ```csharp // ❌ Ambiguous — is this a full path or a relative fragment? void Export(string outputPath) { } // ✅ Clear contract — the caller must provide a resolved path void Export(FullPath outputPath) { } ``` ### Collections and Dictionaries Use `FullPathComparer` when storing paths in sets or dictionaries to get correct OS-aware equality. ```csharp var seen = new HashSet<FullPath>(); // uses default OS comparison var map = new Dictionary<FullPath, int>(); // Explicit comparer if needed var caseSensitiveSet = new HashSet<FullPath>(FullPathComparer.CaseSensitive); ``` ## What to Look For in Reviews | Smell | Refactor to | |---|---| | `string filePath = Path.Combine(root, "sub", "file.txt");` | `FullPath filePath = root / "sub" / "file.txt";` | | `string fullPath = Path.GetFullPath(relative);` | `FullPath fullPath = FullPath.FromPath(relative);` | | `if (path1.Equals(path2, StringComparison.OrdinalIgnoreCase))` | `if (path1 == path2)` (with `FullPath`) | | `path.StartsWith(root)` to check containment | `path.IsChildOf(root)` | | `Path.GetDirectoryName(path)` | `path.Parent` | | `Path.GetFileName(path)` | `path.Name` | | `Path.GetExtension(path)` | `path.Extension` | | `Path.ChangeExtension(path, ext)` | `path.ChangeExtension(ext)` | | `Directory.CreateDirectory(Path.GetDirectoryName(path))` | `path.CreateParentDirectory()` | | `void Foo(string path)` for an absolute-path parameter | `void Foo(FullPath path)` | ## Scope This skill targets **local file-system paths** only. It does not apply to: - URLs or URIs - Cloud/blob storage paths - Database connection strings - Paths that must remain relative by design (e.g., entries inside a ZIP archive)
View on GitHub