Updated on
The C# Path class is a static class in System.IO that reads and builds file and directory path strings. It pulls a file name or an extension out of a path, joins folders into one path, and tells us which separator characters the current platform uses.
It lives in the System.Runtime.dll assembly, and every member is static, so we call it directly as Path.GetFileName() or Path.Combine(). Let’s go through the operations we use most, with the results on both Windows and Linux. Each example is a method that goes inside a class.
Let’s start.
What Is a Path String?
We use a path string to define the file or directory location on a disk, in memory, or on a device. Each platform determines the path format.
Let’s take a look at some examples for path strings:
- “c:\\MyDir\\MyFile.txt” in C# (Windows).Â
- “/MyDir/MyFile.txt” in C# (Linux).
- “\\MyDir\\MySubdir” in C# (Windows).Â
- “/MyDir/MySubdir” in C# (Linux).Â
A path can contain absolute or relative location information. Absolute paths fully specify a location. Relative paths use the current location as the starting point for locating a file.
What Does System.IO.Path Do in .NET?
System.IO.Path is a static class whose methods and fields work on path strings. We never create an instance. We call Path.GetFileName(), Path.Combine() or Path.GetExtension() directly and get a string, a character or a bool back.
Microsoft Learn spells out what the class doesn’t do: “Most members of the Path class do not interact with the file system and do not verify the existence of the file specified by a path string.”
So Path.GetExtension(@"C:\reports\q3.pdf") returns ".pdf" whether that file exists or not, and Path.ChangeExtension() renames nothing on disk.
That makes the class cheap and safe to call anywhere. It also means a valid-looking result proves nothing about the disk.
Two members are exceptions worth remembering: Path.Exists() checks the disk, and Path.GetTempFileName() creates an empty file. When we need to know what is actually there, we ask File, Directory or Path.Exists().
We cover those existence checks, and what each one returns for a missing path, in our article on checking whether a path is a file or a directory. For the temp-file helpers, see our guide to creating a temp file in the temp folder.
Which Path Separator Characters Does C# Use?
C# reads four separator characters from the Path class, and three of them depend on the operating system the code runs on.
Path.DirectorySeparatorChar separates folders. It is \ on Windows and / on Linux and macOS. Path.AltDirectorySeparatorChar is / on every platform, which is why Windows also accepts C:/MyDir/MyFile.txt.
Path.PathSeparator splits a list of paths inside an environment variable such as PATH. It is ; on Windows and : on Linux, so a Linux PATH reads like /usr/bin:/bin.
Path.VolumeSeparatorChar follows the drive letter on Windows, so it is : there. Linux has no drive letters, and the field returns /.
So we don’t type a separator into a path by hand. Path.Combine() and Path.Join() insert the right one for us, while a hard-coded \ stops being a folder break on Linux and becomes part of the file name instead. We read these fields mostly when we take a path apart.
DirectorySeparatorChar field provides a platform-specific character used to separate directory levels in a path string that reflects a hierarchical file system organization:
public char DirectorySeparatorChar()
{
var result = Path.DirectorySeparatorChar;
Console.WriteLine($"Path.DirectorySeparatorChar: '{result}'");
return result;
}
In this case, when we run the example the result is:
Path.DirectorySeparatorChar: '\' // Windows system output Path.DirectorySeparatorChar: '/' // Linux system output
In Windows, there is an alternative Directory separator '/', but in Linux the alternative separator is the same as the DirectorySeparatorChar. We can obtain the alternative separator by using the AltDirectorySeparatorChar field from the Path class:
public char AltDirectorySeparatorChar()
{
var result = Path.AltDirectorySeparatorChar;
Console.WriteLine($"Path.AltDirectorySeparatorChar: '{result}'");
return result;
}
The output:
Path.AltDirectorySeparatorChar: '/' // Windows system output Path.AltDirectorySeparatorChar: '/' // Linux system output
If we use the PathSeparator field, we obtain the platform-specific separator character used to separate path strings in environment variables:
public char PathSeparator()
{
var result = Path.PathSeparator;
Console.WriteLine($"Path.PathSeparator: '{result}'");
return result;
}
The path separator also differs from Windows to Linux:Â
Path.PathSeparator: ';' //Windows system output Path.PathSeparator: ':' //Linux system output
VolumeSeparatorChar provides a platform-specific volume separator character:
public char VolumeSeparatorChar()
{
var result = Path.VolumeSeparatorChar;
Console.WriteLine($"Path.VolumeSeparatorChar: '{result}'");
return result;
}
It differs from Windows to Linux as well:
Path.VolumeSeparatorChar: ':' //Windows system output Path.VolumeSeparatorChar: '/' //Linux system output
How to Get the Invalid Path Characters?
If we want to get an array containing the characters that are not allowed in path names, we can use the GetInvalidPathChars method:
public char[] GetInvalidPathChars()
{
var result = $"Path.GetInvalidPathChars:";
var invalidChars = Path.GetInvalidPathChars();
Console.WriteLine(result);
foreach (var item in invalidChars)
{
Console.Write($"{item} ");
}
Console.WriteLine();
return invalidChars;
}
The GetInvalidPathChars method returns the invalid path characters and we store them in the invalidChars array variable. Then, we iterate over the invalidChars array to write the content in the console. The output is different if we run the code in a Windows system or a Linux system.
On .NET 10, the array holds 33 characters on Windows, the pipe | and the 32 control characters, and a single one on Linux, the null character. Most Path methods don’t check a path against it. Microsoft Learn notes that on .NET, Path.GetFullPath() is the only member that throws for invalid path characters, and in our run on .NET 10 it rejected only the null character: Path.GetFullPath("a|b.txt") returned a full path, and the error came only when we tried to write that file, as an IOException. Path.GetRelativePath() calls GetFullPath() on both of its arguments, so it throws for the null character too.
For a file name rather than a whole path, Path.GetInvalidFileNameChars() is the stricter list. On Windows it returns 41 characters, including \, /, :, * and ?.
Ends in Directory Separator
The EndsInDirectorySeparator method returns a value that indicates whether the path, specified as a read-only span, ends in a directory separator:
public bool EndsInDirectorySeparator(ReadOnlySpan<char> filePath)
{
var result = Path.EndsInDirectorySeparator(filePath);
Console.WriteLine($"EndsInDirectorySeparator('{filePath.ToString()}') returns '{result}'");
return result;
}
This method receives the filePath parameter of the ReadOnlySpan<char> type, and returns a bool result variable, confirming whether the given string path ends in a directory separator. It also has another overload accepting the string parameter instead of ReadOnlySpan<char>.
Get or Change Path Extension
The GetExtension method returns the extension (including the “.”) of the specified path string:
public string GetExtension(string fileName)
{
var extensionString = Path.GetExtension(fileName);
Console.WriteLine($"GetExtension('{fileName}') returns '{extensionString}'");
return extensionString;
}
In this case, we pass the path in the fileName parameter, and GetExtension returns the extension, which we store in extensionString.
If we want to change the path string extension, we can invoke the ChangeExtension method:
public string ChangeExtension(string path, string newExtension)
{
var result = Path.ChangeExtension(path, newExtension);
Console.WriteLine($"ChangeExtension('{path}', '{newExtension}') returns '{result}'");
return result;
}
We invoke the ChangeExtension method to change the path string extension from, for example .com, to a new value .old. Additionally, we store the new path in the result variable to print it in the console.Â
Passing null as the new extension removes it, so myfile.ext becomes myfile.
Combine Strings Into a Path
We can combine two strings into a path by calling the Combine method:
public string Combine(string path1, string path2)
{
var combination = Path.Combine(path1, path2);
Console.WriteLine($"When you combine '{path1}' and '{path2}', the result is: {Environment.NewLine}'{combination}'");
return combination;
}
Also with the Combine method, we can combine three or four strings or even an array of strings into a path string. You can check our source code for these additional implementations.
Get Directory Name
The GetDirectoryName method returns the directory information for the specified path:
public string? GetDirectoryName(string filePath)
{
var directoryName = Path.GetDirectoryName(filePath);
Console.WriteLine($"GetDirectoryName('{filePath}') returns '{directoryName}'");
return directoryName;
}
We call the GetDirectoryName method to extract the directory information from filePath that we accept as a parameter in our GetDirectoryName method.
When the path is a root such as C:\, there is no parent directory, and the method returns null. That is why our method returns string?.
Get File Name
When we want to return the file name for a given file path string, we can use the GetFileName method:
public ReadOnlySpan<char> GetFileName(ReadOnlySpan<char> filePath)
{
ReadOnlySpan<char> fileName = Path.GetFileName(filePath);
Console.WriteLine($"GetFileName('{filePath.ToString()}') returns '{fileName.ToString()}'");
return fileName;
}
This overload takes and returns a ReadOnlySpan<char>, so the file name it hands back is a slice of the input rather than a new string. We cover how that saves allocations in our guide to Span in C#.
Get Full Path String
When we call the GetFullPath method, we use the file or directory as a parameter we want to obtain absolute path information for. This method returns the absolute path for the specified path string:
public string GetFullPath(string mydirPath)
{
var pathResult = Path.GetFullPath(mydirPath);
Console.WriteLine($"GetFullPath('{mydirPath}') returns '{pathResult}'");
return pathResult;
}
This method accepts the mydirPath string path parameter and returns the absolute path string, which we store in the pathResult variable.Â
A relative path resolves against the process’s current directory, not the folder the application runs from. In our run, GetFullPath("mydir") returned a path under the folder dotnet run was started in, while AppContext.BaseDirectory pointed at bin\Debug\net10.0\. Our article on reading a file without specifying the full path shows how to build a path from the application folder instead.
The GetFullPath method has another definition receiving two parameters GetFullPath(string path, string basePath):
public string GetFullPath(string path, string basePath)
{
var pathResult = Path.GetFullPath(path, basePath);
Console.WriteLine($"Fully qualified path:\n {pathResult}");
return pathResult;
}
The first parameter, path, is the relative path to append. The second, basePath, is the fully qualified path it starts from.
Get Path Root
The GetPathRoot method returns the root directory information from the path contained in the specified string variable:
public string? GetPathRoot(string fullPathString)
{
var pathRoot = Path.GetPathRoot(fullPathString);
Console.WriteLine($"GetPathRoot('{fullPathString}') returns '{pathRoot}'");
return pathRoot;
}
When we pass the fullPathString parameter to the GetPathRoot method, we obtain the root directory stored in the pathRoot string variable.
For a relative path such as MyDir\myfile.ext, there is no root, and the method returns an empty string.
Concatenate Path Components
In the Path Class, there are several built methods to concatenate path components. All of them share the same name TryJoin and Join, but they receive different parameters.Â
If we declare the path components with ReadOnlySpan<char> variables, we can use the TryJoin method. This method concatenates two or three path components into a single path string:
public void TryJoin(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, Span<char> destination, out int charsWritten)
{
if (Path.TryJoin(path1, path2, destination, out charsWritten))
Console.WriteLine($"Wrote {charsWritten} characters: '{destination[..charsWritten].ToString()}'");
else
Console.WriteLine("Concatenation operation failed.");
}
Here, we declare two path components path1 and path2 as ReadOnlySpan<char> parameters. We use both parameters with the TryJoin method to concatenate them into a single path string. The result of the concatenation is stored inside the destination variable. The TryJoin method also has an out parameter, charsWritten. This parameter indicates the number of characters written to the destination.Â
When destination is too short for the result, TryJoin returns false rather than throwing, which is the else branch in our method.
Also, it is possible to send two, three, or four path string variables as arguments for obtaining a single path string by using the Join method:
public string Join(string path1, string path2, string path3)
{
var result = Path.Join(path1, path2, path3);
Console.WriteLine($"Path.Join: '{result}'");
return result;
}
In this case, we store the path components in three string variables path1, path2 and path3, and we call the Join method to concatenate them into a single path string.Â
Finally, we can concatenate an array of paths into a single path with Join(string[]) method:
public string Join(string[] pathArrayComponents)
{
var result = Path.Join(pathArrayComponents);
Console.WriteLine($"Path.Join: '{result}'");
return result;
}
What Is the Difference Between Path.Combine and Path.Join?
Path.Combine() and Path.Join() both glue path segments together and add a separator where one is missing. They differ mainly on one kind of input: a segment that is rooted, meaning it starts with a separator or a drive.
Path.Combine() treats a rooted segment as a new starting point and throws away everything before it. Path.Combine(@"C:\uploads", @"\report.pdf") returns \report.pdf, not a file inside C:\uploads.
Path.Join() never does that. It concatenates what we give it, so the same call returns C:\uploads\report.pdf, with the folder intact.
That difference matters when a segment comes from outside our code, such as an uploaded file name or a value from a query string. The Combine() remarks on Microsoft Learn say it directly: “particularly if any subsequent arguments are strings input by the user, call the Join or TryJoin method instead.”
Path.Join() also skips a null segment, where Path.Combine() throws an ArgumentNullException. Both skip an empty string.
Let’s see both methods side by side, with the same two segments:
public (string Combined, string Joined) CombineVersusJoin(string basePath, string fileName)
{
var combined = Path.Combine(basePath, fileName);
var joined = Path.Join(basePath, fileName);
Console.WriteLine($"Combine: '{combined}', Join: '{joined}'");
return (combined, joined);
}
We pass @"C:\uploads" as the folder and @"\report.pdf" as the file name, the shape a user-supplied name can take:
Combine: '\report.pdf', Join: 'C:\uploads\report.pdf' // Windows system output Combine: '/report.pdf', Join: '/uploads/report.pdf' // Linux system output (with "/uploads" and "/report.pdf")
Combine() lost the folder completely. On Windows, \report.pdf now points at the root of the current drive.
Neither method resolves .., though. Path.Join(@"C:\uploads", @"..\secret.txt") returns C:\uploads\..\secret.txt, and Path.GetFullPath() turns that into C:\secret.txt. So Join() on its own does not keep a user inside a folder. When that matters, Microsoft’s guidance for the CA3003 code analysis rule puts one fix first: where we can, limit a user-supplied name to a known list of safe values.
If the name comes from an upload, the extension needs checking as well, which we do in our article on validating file upload extensions in ASP.NET Core.
Which Path Method Returns Which Part of a Path?
Here are eight members from this article, plus four it is worth knowing, most of them applied to one path on each platform: C:\MyDir\MySubDir\myfile.ext on Windows and /MyDir/MySubDir/myfile.ext on Linux.
| Member | Returns | Result |
|---|---|---|
GetFileName(path) | file name with extension | myfile.ext on both |
GetFileNameWithoutExtension(path) | file name only | myfile on both |
GetExtension(path) | extension, with the dot | .ext on both |
HasExtension(path) | whether there is one | True on both |
ChangeExtension(path, ".old") | the path with a new extension | C:\MyDir\MySubDir\myfile.old on Windows, /MyDir/MySubDir/myfile.old on Linux |
GetDirectoryName(path) | the parent folder | C:\MyDir\MySubDir on Windows, /MyDir/MySubDir on Linux |
GetPathRoot(path) | the root | C:\ on Windows, / on Linux |
GetRelativePath(base, path) with base C:\MyDir or /MyDir | path from base to path | MySubDir\myfile.ext on Windows, MySubDir/myfile.ext on Linux |
IsPathFullyQualified(path) | fixed to a drive or root | True on both |
EndsInDirectorySeparator(path) | whether it ends in a separator | False on both |
Combine(folder, name) with folder C:\uploads or /uploads and the rooted name \report.pdf or /report.pdf | segments, reset by a rooted one | \report.pdf on Windows, /report.pdf on Linux |
Join(folder, name), same inputs | segments, concatenated | C:\uploads\report.pdf on Windows, /uploads/report.pdf on Linux |
GetFileNameWithoutExtension(), HasExtension(), GetRelativePath() and IsPathFullyQualified() are the four we haven’t shown above, and each is a one-liner. IsPathFullyQualified() is the stricter cousin of IsPathRooted(): on Windows, \MyDir is rooted but not fully qualified, because it still depends on the current drive.
Every row comes from the tests in our sample, which assert each result on both Windows and Linux.
Conclusion
In this article, we learned how to work with file and directory path information in C#. We saw how to use different members from the Path class to obtain different pieces of information in both Windows and Linux. When part of a path comes from a user, we reach for Path.Join().
Tested with .NET 10.0.10 (SDK 10.0.302) and NUnit 5.0.0.
