How can I compare (directory) paths in C#?

From this answer, this method can handle a few edge cases:

public static string NormalizePath(string path)
{
    return Path.GetFullPath(new Uri(path).LocalPath)
               .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
               .ToUpperInvariant();
}

More details in the original answer. Call it like:

bool pathsEqual = NormalizePath(path1) == NormalizePath(path2);

Should work for both file and directory paths.


There are some short comes to the implementation of paths in .NET. There are many complaints about it. Patrick Smacchia, the creator of NDepend, published an open source library that enables handling of common and complex path operations. If you do a lot of compare operations on paths in your application, this library might be useful to you.


GetFullPath seems to do the work, except for case difference (Path.GetFullPath("test") != Path.GetFullPath("TEST")) and trailing slash. So, the following code should work fine:

String.Compare(
    Path.GetFullPath(path1).TrimEnd('\\'),
    Path.GetFullPath(path2).TrimEnd('\\'), 
    StringComparison.InvariantCultureIgnoreCase)

Or, if you want to start with DirectoryInfo:

String.Compare(
    dirinfo1.FullName.TrimEnd('\\'),
    dirinfo2.FullName.TrimEnd('\\'), 
    StringComparison.InvariantCultureIgnoreCase)

The question has been edited and clarified since it was originally asked and since this answer was originally posted. As the question currently stands, this answer below is not a correct answer. Essentially, the current question is asking for a purely textual path comparison, which is quite different from wanting to determine if two paths resolve to the same file system object. All the other answers, with the exception of Igor Korkhov's, are ultimately based on a textual comparison of two names.

If one actually wants to know when two paths resolve to the same file system object, you must do some IO. Trying to get two "normalized" names, that take in to account the myriad of possible ways of referencing the same file object, is next to impossible. There are issues such as: junctions, symbolic links, network file shares (referencing the same file object in different manners), etc. etc. In fact, every single answer above, with the exception of Igor Korkhov's, will absolutely give incorrect results in certain circumstances to the question "do these two paths reference the same file system object. (e.g. junctions, symbolic links, directory links, etc.)

The question specifically requested that the solution not require any I/O, but if you are going to deal with networked paths, you will absolutely need to do IO: there are cases where it is simply not possible to determine from any local path-string manipulation, whether two file references will reference the same physical file. (This can be easily understood as follows. Suppose a file server has a windows directory junction somewhere within a shared subtree. In this case, a file can be referenced either directly, or through the junction. But the junction resides on, and is resolved by, the file server, and so it is simply impossible for a client to determine, purely through local information, that the two referencing file names refer to the same physical file: the information is simply not available locally to the client. Thus one must absolutely do some minimal IO - e.g. open two file object handles - to determine if the references refer to the same physical file.)

The following solution does some IO, though very minimal, but correctly determines whether two file system references are semantically identical, i.e. reference the same file object. (if neither file specification refers to a valid file object, all bets are off):

public static bool AreDirsEqual(string dirName1, string dirName2, bool resolveJunctionaAndNetworkPaths = true)
{
    if (string.IsNullOrEmpty(dirName1) || string.IsNullOrEmpty(dirName2))
        return dirName1==dirName2;
    dirName1 = NormalizePath(dirName1); //assume NormalizePath normalizes/fixes case and path separators to Path.DirectorySeparatorChar
    dirName2 = NormalizePath(dirName2);
    int i1 = dirName1.Length;
    int i2 = dirName2.Length;
    do
    {
        --i1; --i2;
        if (i1 < 0 || i2 < 0)
            return i1 < 0 && i2 < 0;
    } while (dirName1[i1] == dirName2[i2]);//If you want to deal with international character sets, i.e. if NormalixePath does not fix case, this comparison must be tweaked
    if( !resolveJunctionaAndNetworkPaths )
        return false;
    for(++i1, ++i2; i1 < dirName1.Length; ++i1, ++i2)
    {
        if (dirName1[i1] == Path.DirectorySeparatorChar)
        {
            dirName1 = dirName1.Substring(0, i1);
            dirName2 = dirName1.Substring(0, i2);
            break;
        }
    }
    return AreFileSystemObjectsEqual(dirName1, dirName2);
}

public static bool AreFileSystemObjectsEqual(string dirName1, string dirName2)
{
    //NOTE: we cannot lift the call to GetFileHandle out of this routine, because we _must_
    // have both file handles open simultaneously in order for the objectFileInfo comparison
    // to be guaranteed as valid.
    using (SafeFileHandle directoryHandle1 = GetFileHandle(dirName1), directoryHandle2 = GetFileHandle(dirName2))
    {
        BY_HANDLE_FILE_INFORMATION? objectFileInfo1 = GetFileInfo(directoryHandle1);
        BY_HANDLE_FILE_INFORMATION? objectFileInfo2 = GetFileInfo(directoryHandle2);
        return objectFileInfo1 != null
                && objectFileInfo2 != null
                && (objectFileInfo1.Value.FileIndexHigh == objectFileInfo2.Value.FileIndexHigh)
                && (objectFileInfo1.Value.FileIndexLow == objectFileInfo2.Value.FileIndexLow)
                && (objectFileInfo1.Value.VolumeSerialNumber == objectFileInfo2.Value.VolumeSerialNumber);
    }
}

static SafeFileHandle GetFileHandle(string dirName)
{
    const int FILE_ACCESS_NEITHER = 0;
    //const int FILE_SHARE_READ = 1;
    //const int FILE_SHARE_WRITE = 2;
    //const int FILE_SHARE_DELETE = 4;
    const int FILE_SHARE_ANY = 7;//FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE
    const int CREATION_DISPOSITION_OPEN_EXISTING = 3;
    const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
    return CreateFile(dirName, FILE_ACCESS_NEITHER, FILE_SHARE_ANY, System.IntPtr.Zero, CREATION_DISPOSITION_OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, System.IntPtr.Zero);
}


static BY_HANDLE_FILE_INFORMATION? GetFileInfo(SafeFileHandle directoryHandle)
{
    BY_HANDLE_FILE_INFORMATION objectFileInfo;
    if ((directoryHandle == null) || (!GetFileInformationByHandle(directoryHandle.DangerousGetHandle(), out objectFileInfo)))
    {
        return null;
    }
    return objectFileInfo;
}

[DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, SetLastError = true)]
static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode,
 IntPtr SecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetFileInformationByHandle(IntPtr hFile, out BY_HANDLE_FILE_INFORMATION lpFileInformation);

[StructLayout(LayoutKind.Sequential)]
public struct BY_HANDLE_FILE_INFORMATION
{
    public uint FileAttributes;
    public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
    public uint VolumeSerialNumber;
    public uint FileSizeHigh;
    public uint FileSizeLow;
    public uint NumberOfLinks;
    public uint FileIndexHigh;
    public uint FileIndexLow;
};

Note that in the above code I have included two lines like dirName1 = NormalizePath(dirName1); and have not specified what the function NormalizePath is. NormalizePath can be any path-normalization function - many have been provided in answers elsewhere in this question. Providing a reasonable NormalizePath function means that AreDirsEqual will give a reasonable answer even when the two input paths refer to non-existent file system objects, i.e. to paths that you simply want to compare on a string-level. ( Ishmaeel's comment above should be paid heed as well, and this code does not do that...)

(There may be subtle permissions issues with this code, if a user has only traversal permissions on some initial directories, I am not sure if the file system accesses required by AreFileSystemObjectsEqual are permitted. The parameter resolveJunctionaAndNetworkPaths at least allows the user to revert to pure textual comparison in this case...)

The idea for this came from a reply by Warren Stevens in a similar question I posted on SuperUser: https://superuser.com/a/881966/241981