Simplifying paths handling in .NET code with the FullPath type

  • Gérald Barré

In my projects, I often deal with paths. However, there are many traps when working with path:

  • A path can be absolute or relative (c:\a\b, .\a\b)
  • A path can contain navigation segments (., ..)
  • A path can end with a directory separator or not (c:\a or c:\a\)
  • A path can contain empty segments (c:\a\\b)
  • A path can alternate between multiple directory separators on Windows (c:\a/b\c)
  • A path can be case-sensitive or case-insensitive
  • 2 paths can target the same file/directory using symbolic links/junctions

This means that comparing path is tricky. Also relative paths are harder to debug as the full path depends on the working directory. When you have an issue such as a FileNotFoundException, you are happy when the log file contains the full path to the file.

To avoid this issue, I often use a struct to represent paths: FullPath. This type ensures the path it stores is always an absolute path. It also handles path comparisons and simplifies path manipulation. Let's see how to use it:

// https://www.nuget.org/packages/Meziantou.Framework.FullPath
// <PackageReference Include="Meziantou.Framework.FullPath" Version="1.0.2" />

// Create FullPath
FullPath rootPath = FullPath.FromPath("demo"); // It automatically calls Path.GetFullPath to resolve the path
FullPath filePath = FullPath.Combine(rootPath, "temp", "meziantou.txt"); // Use Path.Combine to join paths (you can combine as many path as you needed)
FullPath temp = FullPath.GetTempPath(); // equivalent of Path.GetTempPath()
FullPath cwd = FullPath.GetCurrentDirectory(); // equivalent of Environment.CurrentDirectory

// Combine path: you can use the / operator to join path
FullPath filePath1 = rootPath / "temp" / "meziantou.txt";

// Compare path
// Comparisons are case-insensitive on Windows and case-sensitive on other operating systems by default
_ = filePath == rootPath;
_ = filePath.Equals(rootPath, ignoreCase: false);

// Get parent directory
FullPath parent = filePath.Parent;

// Get file/directory name - extension
var name = filePath.Name;
var ext = filePath.Extension;

// Make relative path
string relativePath = filePath.MakeRelativeTo(rootPath); // temp\meziantou.txt

// Check if a path is under another path
bool isChildOf = filePath.IsChildOf(rootPath);

// FullPath is implicitly converted to string, so it works well with File/Directory methods
System.IO.File.WriteAllText(filePath, content);

There are multiple advantages to use a struct to represent paths:

  • You always manipulate full paths, so you don't spend time resolving the path manually when logging or debugging

  • You don't mess up with path comparisons, the logic is at a single location

  • You can enrich the type with useful methods such as MakeRelativePath or IsChildOf

  • You take advantage of the type system to ensure you are using a path instead of just a string

    void Sample(string path) { }   // What is expected? a full path or a relative path?
    void Sample(FullPath path) { } // The intent is clear

If you want to use this struct you can get the code on GitHub (FullPath.cs) or you can directly use the NuGet package in your project and use the namespace Meziantou.Framework.FullPath:

<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <PackageReference Include="Meziantou.Framework.FullPath" Version="1.0.2" />
  </ItemGroup>
</Project>

I hope this code will help you working with paths! Feel free to open an issue find a bug or want a new feature.

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee