Skip to content

FileInfo/DirectoryInfo: enable transparent handling of symbolic links #52908

Open

Description

@tmds

Background and Motivation

Users may not be aware that the fileName/path they are providing is a symbolic link.

This proposal makes it possible to operate on such path without the user having to manually handle symbolic links using the APIs added in #24271.

Proposed API

A new constructor on FileInfo allows to specify whether the instance should return information about the target instead of the symbolic link.

class FileInfo
{
   public FileInfo(string fileName, bool followLinks) { }
}
class DirectoryInfo
{
    public DirectoryInfo(string path, bool followLinks) { }
}

The argument causes information for the link target to be returned for the following properties. When there is no target they throw no such file.

class FileInfo
{
    public bool IsReadOnly { get { throw null; } set { } }
    public long Length { get { throw null; } }
}
class FileSystemInfo
{
    public FileAttributes Attributes { get { throw null; } set { } }
    public DateTime CreationTime { get { throw null; } set { } }
    public DateTime CreationTimeUtc { get { throw null; } set { } }
    public abstract bool Exists { get; }
    public DateTime LastAccessTime { get { throw null; } set { } }
    public DateTime LastAccessTimeUtc { get { throw null; } set { } }
    public DateTime LastWriteTime { get { throw null; } set { } }
    public DateTime LastWriteTimeUtc { get { throw null; } set { } }

    // from https://github.com/dotnet/runtime/issues/24271
    // always returns `false` when `followLinks` is `true`.
    public bool IsSymbolicLink { get; } 
}

It does not affect the following operations. Which are either:

  • always working on the target, or
  • working on the 'fileName'/'path' argument.
class DirectoryInfo
{
    // applies to 'path':
    public DirectoryInfo? Parent { get { throw null; } }
    public DirectoryInfo Root { get { throw null; } }

    // applies to 'target':
    public void Create() { }
    public DirectoryInfo CreateSubdirectory(string path) { throw null; }
    public IEnumerable<DirectoryInfo> EnumerateDirectories() { throw null; }
    public IEnumerable<DirectoryInfo> EnumerateDirectories(string searchPattern) { throw null; }
    public IEnumerable<DirectoryInfo> EnumerateDirectories(string searchPattern, EnumerationOptions enumerationOptions) { throw null; }
    public IEnumerable<DirectoryInfo> EnumerateDirectories(string searchPattern, SearchOption searchOption) { throw null; }
    public IEnumerable<FileInfo> EnumerateFiles() { throw null; }
    public IEnumerable<FileInfo> EnumerateFiles(string searchPattern) { throw null; }
    public IEnumerable<FileInfo> EnumerateFiles(string searchPattern, EnumerationOptions enumerationOptions) { throw null; }
    public IEnumerable<FileInfo> EnumerateFiles(string searchPattern, SearchOption searchOption) { throw null; }
    public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos() { throw null; }
    public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string searchPattern) { throw null; }
    public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string searchPattern, EnumerationOptions enumerationOptions) { throw null; }
    public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string searchPattern, SearchOption searchOption) { throw null; }
    public DirectoryInfo[] GetDirectories() { throw null; }
    public DirectoryInfo[] GetDirectories(string searchPattern) { throw null; }
    public DirectoryInfo[] GetDirectories(string searchPattern, EnumerationOptions enumerationOptions) { throw null; }
    public DirectoryInfo[] GetDirectories(string searchPattern, SearchOption searchOption) { throw null; }
    public FileInfo[] GetFiles() { throw null; }
    public FileInfo[] GetFiles(string searchPattern) { throw null; }
    public FileInfo[] GetFiles(string searchPattern, EnumerationOptions enumerationOptions) { throw null; }
    public FileInfo[] GetFiles(string searchPattern, SearchOption searchOption) { throw null; }
    public FileSystemInfo[] GetFileSystemInfos() { throw null; }
    public FileSystemInfo[] GetFileSystemInfos(string searchPattern) { throw null; }
    public FileSystemInfo[] GetFileSystemInfos(string searchPattern, EnumerationOptions enumerationOptions) { throw null; }
    public FileSystemInfo[] GetFileSystemInfos(string searchPattern, SearchOption searchOption) { throw null; }

    // applies to 'path':
    public void Delete(bool recursive) { }
    public override void Delete() { }
    public void MoveTo(string destDirName) { }
}
class FileInfo
{
    // applies to 'fileName'
    public DirectoryInfo? Directory { get { throw null; } }
    public string? DirectoryName { get { throw null; } }

    // applies to 'target':
    public StreamWriter AppendText() { throw null; }
    public FileInfo CopyTo(string destFileName) { throw null; }
    public FileInfo CopyTo(string destFileName, bool overwrite) { throw null; }
    public FileStream Create() { throw null; }
    public StreamWriter CreateText() { throw null; }
    public void Decrypt() { }
    public void Encrypt() { }
    public FileStream Open(FileMode mode) { throw null; }
    public FileStream Open(FileMode mode, FileAccess access) { throw null; }
    public FileStream Open(FileMode mode, FileAccess access, FileShare share) { throw null; }
    public FileStream OpenRead() { throw null; }
    public StreamReader OpenText() { throw null; }
    public FileStream OpenWrite() { throw null; }
    public FileInfo Replace(string destinationFileName, string? destinationBackupFileName) { throw null; }
    public FileInfo Replace(string destinationFileName, string? destinationBackupFileName, bool ignoreMetadataErrors) { throw null; }

    // applies to 'path':
    public override void Delete() { }
    public void MoveTo(string destFileName) { }
    public void MoveTo(string destFileName, bool overwrite) { }
}
class FileSystemInfo
{
    // applies to 'fileName'/'path':
    protected string FullPath;
    protected string OriginalPath;
    public string Extension { get { throw null; } }
    public virtual string FullName { get { throw null; } }
    public abstract string Name { get; }
}

Usage Examples

FileInfo file = new FileInfo("/tmp/my-file", followLinks: true);
DateTime lastWriteTime = file.LastWriteTimeUtc;
while (true)
{
    Thread.Sleep(1000);
    file.Refresh();
    DateTime writeTime = file.LastWriteTimeUtc;
    if (writeTime != lastWriteTime)
    {
        Console.WriteLine("The file has changed");
        lastWriteTime = writeTime;
    }
}

Implementation

On Linux, this means the information for the properties is retrieved using stat instead of vstat.
For Delete, the final target is first located, e.g. by calling realpath, and that is then deleted.

edits:

  • added IsSymbolicLink from Proposed API for symbolic links #24271
  • renamed followLink to followLinks.
  • updated proposal to make Delete target based.
  • added implementation section which covers Linux
  • revert Delete to be path based again.
  • place MoveTo under path-based

Activity

  1. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on May 18, 2021
  2. ghost added
    untriagedNew issue has not been triaged by the area owner
    on May 18, 2021
  3. mklement0 commented on May 19, 2021

    @mklement0
  4. tmds commented on May 19, 2021

    @tmds
    MemberAuthor

    I think it is problematic to create hybrid FileSystemInfo instances that combine properties of the link (.Name, .FullName, ...) with properties of its target.

    I don't think this is problematic. I think it is desired to not expose a symlink was involved.
    The path that is passed to the constructor is treated as an invariant.

    What happens if the target doesn't exist?

    No such file exception.

    What is the implied recursion mode of followLink: true?

    Return information about the final target.

    On the other hand, (c) is usually supported by a single platform-native call, such as GetFinalPathNameByHandle on Windows and realpath() function on Unix-like platforms , so from a performance perspective it may be preferable.

    For Unix, it should be about using stat instead of lstat. I don't think we need realpath.
    I don't know about Windows.

  5. iSazonov commented on May 19, 2021

    @iSazonov
    Contributor

    This seems less flexible and more confusing than #24271. Especially disturbing is that we set the behavior "before", but then we don't even know about it (i.e. if then there are two code paths, we can't choose the right one). In other words it's not so easy to implement the "find all symbolic links" scenario.

  6. tmds commented on May 19, 2021

    @tmds
    MemberAuthor

    @iSazonov this is about working with files and directories regardless whether the path is or is not using a symbolic link.
    #24271 is about working with symbolic links.
    These are different use-cases.

  7. mklement0 commented on May 19, 2021

    @mklement0
  8. tmds commented on May 19, 2021

    @tmds
    MemberAuthor

    However, I think the proposed hybrid file-info approach is problematic, because it violates a fundamental assumption: that the properties of a given FileSystemInfo represent the file-system entry pointed to by its path.

    How should these properties behave when the path does not exist, or when the link is broken?
    Should these properties update on Refresh?

    I think it is valid for these properties to reflect the path that was passed to the constructor and maintain the behavior that has always existed here. Changing that can also cause problems.

    If the final target is desired, maybe add a property for that. For example: string RealPath { get; } (which can call realpath on Linux).

    As proposed, one wouldn't be able to tell whether a given FileSystemInfo instance reflects the link's or the target's properties

    They reflect the target independent of whether the path is a link or not.

  9. mklement0 commented on May 19, 2021

    @mklement0
  10. tmds commented on May 19, 2021

    @tmds
    MemberAuthor

    Thus, setting followLink to true would simply return a FileSystemInfo instance representing the (ultimate, non-link) target, as if that target had been requested directly.

    So the constructor throws if the path does not exist, or the link is broken?
    And Refresh would be performed against the target path?

    If I ask for link ./foo with followLink = true, and the resulting FileInfo instance then tells me that ./foo isn't a link, that is a problem.

    I don't understand the problem. followLink is meant to make the properties reflect the target of the link, not the link itself.

    you shouldn't have to guess at / investigate whether its properties are a 1:1 or a a hybrid representation.

    Some properties are proposed to keep matching the path that was passed to the constructor. There is no guessing.

  11. mklement0 commented on May 19, 2021

    @mklement0
  12. iSazonov commented on May 20, 2021

    @iSazonov
    Contributor

    @iSazonov this is about working with files and directories regardless whether the path is or is not using a symbolic link.
    #24271 is about working with symbolic links.
    These are different use-cases.

    It looks like an insidious trick :-) when some properties and methods return information about the source object and others about the target object. This approach opens up unlimited ways for bugs in applications.
    This is all the more useless because most APIs already work transparently with symbolic links.

    (And please add more use-cases in OP - your current demo says nothing.)

  13. tmds commented on May 20, 2021

    @tmds
    MemberAuthor

    If the target doesn't exist: That makes sense to me, and I thought it was also what you were proposing.

    I'd take the path as is, just like it happens now. FileInfo constructor doesn't throw if the path does not exist.

    This approach opens up unlimited ways for bugs in applications.

    @iSazonov you say this causes bugs. @mklement0 you call it problematic. Can you demonstrate issues with some code?

    I propose to keep invariant that has existed between the constructor argument and properties. Changing those definitely creates opportunity for bugs with existing code.

    I don't see the value of making these properties reflect the target path. .Get[Link]Target(returnFinalTarget: true) can be used to get it.

    This is all the more useless because most APIs already work transparently with symbolic links.

    Except the ones that are part of this proposal.

    And please add more use-cases

    Other example:

    const string filename = "/tmp/filename";
    File.WriteAllText(filename, "content");
    
    var fi = new FileInfo(filename, followsLink: true);
    Console.WriteLine("File length is " + fi.Length);
    
    // Make path a symbolic link to the same content.
    const string target = "/tmp/target";
    File.Move(filename, target);
    File.CreateLink(target, filename); // use API from https://github.com/dotnet/runtime/issues/24271
    
    fi.Refresh();
    Console.WriteLine("File length is " + fi.Length);
  14. 16 remaining items

  15. added
    needs-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration
    on Jul 1, 2021
  16. added this to the Future milestone on Jul 1, 2021
  17. jozkee commented on Jul 23, 2021

    @jozkee
    Member

    I wonder if we could instead add APIs for querying link's target file/directory information and return that in a minimal wrapper (FileSystemEntry) and keep the APIs focused in doing as less allocations as possible.
    We could have the following APIs:

    public static class File
    {
        public static FileSystemEntry GetFinalInformation(ReadOnlySpan<char> path);
    }
    
    public static class Directory
    {
        public static FileSystemEntry GetFinalInformation(ReadOnlySpan<char> path);
    }
    
    public class FileSystemInfo
    {
        public FileSystemEntry FinalInformation => default;
    }

    Notes:

    • FileSystemEntry is ref struct and also is contained in the System.IO.Enumeraiton namespace, therefore it may not be a good idea to use it as the return type.
    • We could create a type very similar to FileSystemEntry that remains as minimal as possible:
    public readonly struct MinimalFileSystemInfo
    {
        // On Windows this can be obtained with GetFileInformationByHandleEx(lpFileInformation: FileBasicInfo).
        // On Unix this can be obtained with stat.
        public DateTimeOffset CreationTimeUtc;
        public DateTimeOffset LastAccessTimeUtc;
        public DateTimeOffset LastWriteTimeUtc;
        public FileAttributes Attributes;
    }

    cc @ericstj @carlossanlop

  18. ericstj commented on Jul 23, 2021

    @ericstj
    Member

    FileSystemEntry is ref struct and also is contained in the System.IO.Enumeraiton namespace, therefore it may not be a good idea to use it as the return type.

    Would it make sense to consider this scenario as an extension of System.IO.Enumeraiton if that namespace is already dealing with this type of thing for non-symlinks?

  19. jozkee commented on Jul 23, 2021

    @jozkee
    Member

    if that namespace is already dealing with this type of thing for non-symlinks?

    Do you mean if FileSystemEntry already returns the information of the link target? If so, it does not, it returns information for the link itself.

  20. ericstj commented on Jul 23, 2021

    @ericstj
    Member

    I mean why not add link target support to the enumeration APIs.

  21. jozkee commented on Jul 23, 2021

    @jozkee
    Member

    We can add support for enumeration scenarios as well, I was thinkking in an instance method that returns a new FileSystemEntry or returns the same instance if not a link:

    public ref struct FileSystemEntry
    {
        public FileSystemEntry GetFinalLinkTarget();
    }

    I don't know if we should care for the non-final target scenario.

  22. iSazonov commented on Jul 26, 2021

    @iSazonov
    Contributor

    I think an enhancement of FileSystemEntry looks right direction as implementation details for both enumeration and explicit (sym)link manipulations. See my old comment #52666 (comment).
    To resolve conflicts between properties (like LastAccessTimeUtc) of link itself and link target it would great to have public class SymbolicLink.

  23. tmds commented on Aug 17, 2021

    @tmds
    MemberAuthor

    I wonder if we could instead add APIs for querying link's target file/directory information and return that in a minimal wrapper (FileSystemEntry) and keep the APIs focused in doing as less allocations as possible.

    My focus is to eliminate the system calls that hop links in ResolveLinkTarget and perform a single stat call.
    For FileInfo I don't think there are many allocations besides the FileInfo instance itself.

    FileInfo being a class makes it usable in more cases than a ref struct.
    Using FileInfo also makes it easy to change existing code where the intent was to get info for the target rather than the link.

    System.IO.Enumeration may also need improving/extending to better support links. So maybe both are warranted.

  24. dotnet-policy-service commented on Feb 16, 2026

    @dotnet-policy-service
    Contributor

    Due to lack of recent activity, this issue has been marked as a candidate for backlog cleanup. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will undo this process.

    This process is part of our issue cleanup automation.

  25. iSazonov commented on Feb 17, 2026

    @iSazonov
    Contributor

    This could be useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.IOneeds-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions