| 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
# .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
FullPath path = FullPath.FromPath("demo");
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.
FullPath root = FullPath.FromPath("/repo");
FullPath file = root / "src" / "Program.cs";
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.
if (pathA == pathB) { }
bool equal = pathA.Equals(pathB, ignoreCase: false);
Navigating the Path Tree
FullPath file = FullPath.FromPath("/repo/src/Program.cs");
FullPath dir = file.Parent;
string name = file.Name;
string nameNoExt = file.NameWithoutExtension;
string ext = file.Extension;
FullPath renamed = file.ChangeExtension(".vb");
Checking Hierarchy
FullPath root = FullPath.FromPath("/repo");
FullPath file = root / "src" / "Program.cs";
bool isChild = file.IsChildOf(root);
Making a Relative Path
FullPath root = FullPath.FromPath("/repo");
FullPath file = root / "src" / "Program.cs";
string relative = file.MakePathRelativeTo(root);
Walking Up the Tree
FullPath start = FullPath.FromPath("/repo/src/deep/nested");
if (start.TryFindFirstAncestorOrSelf(
p => File.Exists(p / ".editorconfig"), out FullPath match))
{
}
if (start.TryFindGitRepositoryRoot(out FullPath gitRoot))
{
}
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.
FullPath config = root / "appsettings.json";
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):
logger.LogInformation("Loading config from {Path}", config.Value);
Ensuring the Parent Directory Exists
FullPath output = root / "artifacts" / "report.html";
output.CreateParentDirectory();
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.
public sealed class AppSettings
{
public FullPath OutputDirectory { get; set; }
}
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.
void Export(string outputPath) { }
void Export(FullPath outputPath) { }
Collections and Dictionaries
Use FullPathComparer when storing paths in sets or dictionaries to get correct OS-aware equality.
var seen = new HashSet<FullPath>();
var map = new Dictionary<FullPath, int>();
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)