Best way to determine if two path reference to same file in C#

In the upcoming Java7, there is a new API to check if two file object are same file reference.

Are there similar API provided in the .NET framework?

I've search it over MSDN but nothing enlighten me.

I want it simple but I don't want to compare by filename which will cause problems with hard/symbolic links and different style of path. (e.g. \\?\C:\, C:\).

What I going to do is just prevent duplicated file being drag and dropped to my linklist.


Solution 1:

As far as I can see (1) (2) (3) (4), the way JDK7 does it, is by calling GetFileInformationByHandle on the files and comparing dwVolumeSerialNumber, nFileIndexHigh and nFileIndexLow.

Per MSDN:

You can compare the VolumeSerialNumber and FileIndex members returned in the BY_HANDLE_FILE_INFORMATION structure to determine if two paths map to the same target; for example, you can compare two file paths and determine if they map to the same directory.

I do not think this function is wrapped by .NET, so you will have to use P/Invoke.

It might or might not work for network files. According to MSDN:

Depending on the underlying network components of the operating system and the type of server connected to, the GetFileInformationByHandle function may fail, return partial information, or full information for the given file.

A quick test shows that it works as expected (same values) with a symbolic link on a Linux system connected using SMB/Samba, but that it cannot detect that a file is the same when accessed using different shares that point to the same file (FileIndex is the same, but VolumeSerialNumber differs).

Solution 2:

Edit: Note that @Rasmus Faber mentions the GetFileInformationByHandle function in the Win32 api, and this does what you want, check and upvote his answer for more information.


I think you need an OS function to give you the information you want, otherwise it's going to have some false negatives whatever you do.

For instance, does these refer to the same file?

  • \server\share\path\filename.txt
  • \server\d$\temp\path\filename.txt

I would examine how critical it is for you to not have duplicate files in your list, and then just do some best effort.

Having said that, there is a method in the Path class that can do some of the work: Path.GetFullPath, it will at least expand the path to long names, according to the existing structure. Afterwards you just compare the strings. It won't be foolproof though, and won't handle the two links above in my example.

Solution 3:

Here is a C# implementation of IsSameFile using GetFileInformationByHandle:

NativeMethods.cs

public static class NativeMethods
{
  [StructLayout(LayoutKind.Explicit)]
  public struct BY_HANDLE_FILE_INFORMATION
  {
    [FieldOffset(0)]
    public uint FileAttributes;

    [FieldOffset(4)]
    public FILETIME CreationTime;

    [FieldOffset(12)]
    public FILETIME LastAccessTime;

    [FieldOffset(20)]
    public FILETIME LastWriteTime;

    [FieldOffset(28)]
    public uint VolumeSerialNumber;

    [FieldOffset(32)]
    public uint FileSizeHigh;

    [FieldOffset(36)]
    public uint FileSizeLow;

    [FieldOffset(40)]
    public uint NumberOfLinks;

    [FieldOffset(44)]
    public uint FileIndexHigh;

    [FieldOffset(48)]
    public uint FileIndexLow;
  }

  [DllImport("kernel32.dll", SetLastError = true)]
  public static extern bool GetFileInformationByHandle(SafeFileHandle hFile, out BY_HANDLE_FILE_INFORMATION lpFileInformation);

  [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  public static extern SafeFileHandle CreateFile([MarshalAs(UnmanagedType.LPTStr)] string filename,
    [MarshalAs(UnmanagedType.U4)] FileAccess access,
    [MarshalAs(UnmanagedType.U4)] FileShare share,
    IntPtr securityAttributes,
    [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
    [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
    IntPtr templateFile);
}

PathUtility.cs

public static bool IsSameFile(string path1, string path2)
{
  using (SafeFileHandle sfh1 = NativeMethods.CreateFile(path1, FileAccess.Read, FileShare.ReadWrite, 
      IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
  {
    if (sfh1.IsInvalid)
      Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

    using (SafeFileHandle sfh2 = NativeMethods.CreateFile(path2, FileAccess.Read, FileShare.ReadWrite,
      IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
    {
      if (sfh2.IsInvalid)
        Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

      NativeMethods.BY_HANDLE_FILE_INFORMATION fileInfo1;
      bool result1 = NativeMethods.GetFileInformationByHandle(sfh1, out fileInfo1);
      if (!result1)
        throw new IOException(string.Format("GetFileInformationByHandle has failed on {0}", path1));

      NativeMethods.BY_HANDLE_FILE_INFORMATION fileInfo2;
      bool result2 = NativeMethods.GetFileInformationByHandle(sfh2, out fileInfo2);
      if (!result2)
        throw new IOException(string.Format("GetFileInformationByHandle has failed on {0}", path2));

      return fileInfo1.VolumeSerialNumber == fileInfo2.VolumeSerialNumber
        && fileInfo1.FileIndexHigh == fileInfo2.FileIndexHigh
        && fileInfo1.FileIndexLow == fileInfo2.FileIndexLow;
    }
  }
}