Path.Combine
filename concatenation
backslash issue
Path.DirectorySeparatorChar
.NET path handling

Why does Path.Combine not properly concatenate filenames that start with a backslash Path.DirectorySeparatorChar?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Path.Combine in .NET discards all preceding path segments when any subsequent argument is an absolute path (starts with \, /, or a drive letter like C:\). This is by design, not a bug — it follows the POSIX and Windows convention that an absolute path component overrides everything before it. The fix is to strip the leading separator from the second argument before combining, or use Path.Join (.NET Core 3.0+) which always concatenates without discarding segments.

The Problem

csharp
1string result = Path.Combine(@"C:\Users\Alice", @"\Documents\file.txt");
2Console.WriteLine(result);
3// Output: \Documents\file.txt
4// Expected: C:\Users\Alice\Documents\file.txt

The second argument \Documents\file.txt starts with \, which .NET treats as a root-relative path. Path.Combine discards C:\Users\Alice and returns only the second argument.

How Path.Combine Decides

Path.Combine processes arguments left to right. For each argument, it checks:

  1. Is this argument a rooted/absolute path?
  2. If yes, discard everything accumulated so far and start fresh from this argument.
  3. If no, append it to the current result with a separator.
csharp
1// Second argument is rooted — first is discarded
2Path.Combine("folder", @"\file.txt")
3// Result: \file.txt
4
5// Second argument is NOT rooted — properly combined
6Path.Combine("folder", "file.txt")
7// Result: folder\file.txt
8
9// Third argument is rooted — first two are discarded
10Path.Combine("C:", "Users", @"\Alice")
11// Result: \Alice
12
13// Drive letter is also rooted
14Path.Combine(@"C:\Projects", @"D:\Other")
15// Result: D:\Other

Fix 1: Strip the Leading Separator

csharp
1string basePath = @"C:\Users\Alice";
2string relativePath = @"\Documents\file.txt";
3
4// Remove leading directory separator
5string cleaned = relativePath.TrimStart(
6    Path.DirectorySeparatorChar,
7    Path.AltDirectorySeparatorChar
8);
9
10string result = Path.Combine(basePath, cleaned);
11Console.WriteLine(result);
12// C:\Users\Alice\Documents\file.txt

Helper Method

csharp
1public static string SafeCombine(params string[] paths)
2{
3    if (paths.Length == 0) return string.Empty;
4
5    string result = paths[0];
6    for (int i = 1; i < paths.Length; i++)
7    {
8        string segment = paths[i].TrimStart(
9            Path.DirectorySeparatorChar,
10            Path.AltDirectorySeparatorChar
11        );
12        result = Path.Combine(result, segment);
13    }
14    return result;
15}
16
17// Usage
18string path = SafeCombine(@"C:\Users", @"\Alice", @"\Documents", "file.txt");
19// C:\Users\Alice\Documents\file.txt

Fix 2: Use Path.Join (.NET Core 3.0+)

Path.Join always concatenates without discarding segments:

csharp
1string result = Path.Join(@"C:\Users\Alice", @"\Documents\file.txt");
2Console.WriteLine(result);
3// C:\Users\Alice\Documents\file.txt
4
5// Path.Join does NOT discard rooted segments
6Path.Join("folder", @"\file.txt");
7// folder\file.txt
8
9// Handles multiple segments
10Path.Join("C:", "Users", "Alice", "Documents");
11// C:\Users\Alice\Documents

Path.Join is the recommended replacement for Path.Combine when you always want concatenation behavior.

Comparison: Path.Combine vs Path.Join

csharp
1string a = @"C:\Base";
2string b = @"\Relative\file.txt";
3string c = "plain.txt";
4
5Console.WriteLine(Path.Combine(a, b));  // \Relative\file.txt (base discarded!)
6Console.WriteLine(Path.Join(a, b));     // C:\Base\Relative\file.txt
7
8Console.WriteLine(Path.Combine(a, c));  // C:\Base\plain.txt
9Console.WriteLine(Path.Join(a, c));     // C:\Base\plain.txt
10
11// With drive letter
12string d = @"D:\Other";
13Console.WriteLine(Path.Combine(a, d));  // D:\Other (base discarded!)
14Console.WriteLine(Path.Join(a, d));     // C:\Base\D:\Other (concatenated literally)
MethodLeading `` in argDrive letter in argAvailable since
Path.CombineDiscards previous segmentsDiscards previous segments.NET 1.0
Path.JoinConcatenates as-isConcatenates as-is.NET Core 3.0

Real-World Scenario: User-Supplied Paths

csharp
1// User supplies a path from a config file or API
2string uploadDir = @"C:\Uploads";
3string userPath = GetPathFromUser();  // Might be "\..\..\Windows\System32"
4
5// DANGEROUS: Path.Combine with a rooted user path
6string bad = Path.Combine(uploadDir, userPath);
7// Could resolve to \..\..\Windows\System32 — path traversal!
8
9// SAFE: Strip root, normalize, and validate
10string safe = Path.Combine(uploadDir,
11    userPath.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
12
13string fullPath = Path.GetFullPath(safe);
14
15// Verify the result is still under uploadDir
16if (!fullPath.StartsWith(uploadDir, StringComparison.OrdinalIgnoreCase))
17{
18    throw new UnauthorizedAccessException("Path traversal detected");
19}

Checking If a Path Is Rooted

csharp
1Console.WriteLine(Path.IsPathRooted(@"\file.txt"));      // True
2Console.WriteLine(Path.IsPathRooted(@"C:\file.txt"));    // True
3Console.WriteLine(Path.IsPathRooted(@"/file.txt"));      // True
4Console.WriteLine(Path.IsPathRooted(@"file.txt"));       // False
5Console.WriteLine(Path.IsPathRooted(@"sub\file.txt"));   // False

Use Path.IsPathRooted to check whether a segment will cause Path.Combine to discard previous segments.

Common Pitfalls

  • Assuming Path.Combine always concatenates: It does not. Any rooted argument (starting with \, /, or a drive letter) causes all previous segments to be discarded. Verify inputs or use Path.Join.
  • Not validating user-supplied paths: User paths like \..\..\etc\passwd combined with Path.Combine can escape the intended directory. Always validate that the resolved full path starts with the expected base directory.
  • Using Path.Join on .NET Framework: Path.Join is only available in .NET Core 3.0+ and .NET 5+. On .NET Framework, use the TrimStart approach or the SafeCombine helper.
  • Hardcoding \\ instead of using Path.DirectorySeparatorChar: On Linux/macOS running .NET, the separator is /, not \. Use Path.DirectorySeparatorChar or Path.Combine/Path.Join for cross-platform compatibility.
  • Double separators: Path.Combine("C:\\folder\\", "file.txt") produces C:\folder\file.txt (no double backslash). But manual string concatenation with + can produce C:\folder\\file.txt. Always use Path.Combine or Path.Join instead of string concatenation for paths.

Summary

  • Path.Combine discards all preceding segments when an argument starts with \, /, or a drive letter
  • This is by design — rooted paths are treated as absolute references that override the base
  • Strip leading separators with TrimStart(Path.DirectorySeparatorChar) before combining
  • Use Path.Join (.NET Core 3.0+) for always-concatenate behavior
  • Always validate combined paths against path traversal attacks when dealing with user input

Course illustration
Course illustration

All Rights Reserved.