[API Proposal]: Add an option to create parent directories in `System.IO.File` APIs
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Stale
- Tech stack
- csharp
- Domain
- api, operating-systems
Research direction
Start with the System.IO.File creation-related APIs and the proposed overload list, then work through the open questions about Open, OpenHandle, symbolic links, and copy or move operations. Done means the API scope, option name, failure behavior, and compatibility implications are resolved well enough for an implementation plan.
Written by the indexing model from the issue text.
Description
Background and motivation
I find it a common task to create (then subsequently write into) a file, given a complete path to be saved.
For example, when given a list of relative paths to files, I might want to save all the files while preserving the directory hierarchy, which might look like this:
/foo/a/1/blah.txt -> C:\archive\foo\a\1\blah.txt
/foo/a/2/blah.txt -> C:\archive\foo\a\2\blah.txt
/foo/b/1/blah.txt -> ...
/foo/b/2/blah.txt -> ...
/bar/a/1/blah.txt -> ...
The APIs under the System.IO.File class is usually good enough for quick and dirty codes that involve simple read/write/creations of files (e.g., scripting)
However, one needs to make sure that all of the parent directories exist, otherwise they get hit with a DirectoryNotFoundException in the face when part of the directories are missing:
Popular workaround this involves the use of FileInfo, accessing its Directory property to call Create which ensures that all the subdirectories leading to the file exists (and does nothing if they already exist)
string filePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Some", "NonExistentFolder", "a.mp4");
(new FileInfo(filePath)).Directory.Create()
File.WriteAllText(filePath, text)
(See https://stackoverflow.com/questions/2955402/how-do-i-create-directory-if-it-doesnt-exist-to-create-a-file)
But to me this feels weird because I'm creating a FileInfo and a DirectoryInfo object which then I immediately throw away.
An alternative would be to create the directory without the FileInfo, which is also suggested in the above StackOverflow post:
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
File.WriteAllText(filePath, text)
Looking at libraries from different languages / frameworks, Node.js seems to have a popular file system library, fs-extra, that has a method called outputFile which creates the parent directory if it does not exist.
I feel like I'm running into this problem quite frequently in a lot of different places; Often when a file creation fails because of missing directories I just want it to create one. It sure would be nice if there is a way to make these methods create parent directories if it does not exist...
API Proposal
namespace System.IO;
public static class File
{
// Existing APIs
public static void AppendAllLines(string path, IEnumerable<string> contents);
public static void AppendAllLines(string path, IEnumerable<string> contents, Encoding encoding);
public static Task AppendAllLinesAsync(string path, IEnumerable<string> contents, Encoding encoding, CancellationToken cancellationToken = default);
public static Task AppendAllLinesAsync(string path, IEnumerable<string> contents, CancellationToken cancellationToken = default);
public static void AppendAllText(string path, string? contents);
public static void AppendAllText(string path, string? contents, Encoding encoding);
public static Task AppendAllTextAsync(string path, string? contents, Encoding encoding, CancellationToken cancellationToken = default);
public static Task AppendAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default);
public static StreamWriter AppendText(string path);
public static FileStream Create(string path, int bufferSize, FileOptions options);
public static FileStream Create(string path);
public static FileStream Create(string path, int bufferSize);
public static StreamWriter CreateText(string path);
public static FileStream OpenWrite(string path);
public static void WriteAllBytes(string path, byte[] bytes);
public static Task WriteAllBytesAsync(string path, byte[] bytes, CancellationToken cancellationToken = default);
public static void WriteAllLines(string path, IEnumerable<string> contents);
public static void WriteAllLines(string path, IEnumerable<string> contents, Encoding encoding);
public static void WriteAllLines(string path, string[] contents);
public static void WriteAllLines(string path, string[] contents, Encoding encoding);
public static Task WriteAllLinesAsync(string path, IEnumerable<string> contents, Encoding encoding, CancellationToken cancellationToken = default);
public static Task WriteAllLinesAsync(string path, IEnumerable<string> contents, CancellationToken cancellationToken = default);
public static void WriteAllText(string path, string? contents);
public static void WriteAllText(string path, string? contents, Encoding encoding);
public static Task WriteAllTextAsync(string path, string? contents, Encoding encoding, CancellationToken cancellationToken = default);
public static Task WriteAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default);
// New APIs
+ public static void AppendAllLines(string path, IEnumerable<string> contents, bool createDirectories);
+ public static void AppendAllLines(string path, IEnumerable<string> contents, Encoding encoding, bool createDirectories);
+ public static Task AppendAllLinesAsync(string path, IEnumerable<string> contents, Encoding encoding, bool createDirectories, CancellationToken cancellationToken = default);
+ public static Task AppendAllLinesAsync(string path, IEnumerable<string> contents, bool createDirectories, CancellationToken cancellationToken = default);
+ public static void AppendAllText(string path, string? contents, bool createDirectories);
+ public static void AppendAllText(string path, string? contents, Encoding encoding, bool createDirectories);
+ public static Task AppendAllTextAsync(string path, string? contents, Encoding encoding, bool createDirectories, CancellationToken cancellationToken = default);
+ public static Task AppendAllTextAsync(string path, string? contents, bool createDirectories, CancellationToken cancellationToken = default);
+ public static StreamWriter AppendText(string path, bool createDirectories);
+ public static FileStream Create(string path, int bufferSize, FileOptions options, bool createDirectories);
+ public static FileStream Create(string path, bool createDirectories);
+ public static FileStream Create(string path, int bufferSize, bool createDirectories);
+ public static StreamWriter CreateText(string path, bool createDirectories);
+ public static FileStream OpenWrite(string path, bool createDirectories);
+ public static void WriteAllBytes(string path, byte[] bytes, bool createDirectories);
+ public static Task WriteAllBytesAsync(string path, byte[] bytes, bool createDirectories, CancellationToken cancellationToken = default);
+ public static void WriteAllLines(string path, IEnumerable<string> contents, bool createDirectories);
+ public static void WriteAllLines(string path, IEnumerable<string> contents, Encoding encoding, bool createDirectories);
+ public static void WriteAllLines(string path, string[] contents, bool createDirectories);
+ public static void WriteAllLines(string path, string[] contents, Encoding encoding, bool createDirectories);
+ public static Task WriteAllLinesAsync(string path, IEnumerable<string> contents, Encoding encoding, bool createDirectories, CancellationToken cancellationToken = default);
+ public static Task WriteAllLinesAsync(string path, IEnumerable<string> contents, bool createDirectories, CancellationToken cancellationToken = default);
+ public static void WriteAllText(string path, string? contents, bool createDirectories);
+ public static void WriteAllText(string path, string? contents, Encoding encoding, bool createDirectories);
+ public static Task WriteAllTextAsync(string path, string? contents, Encoding encoding, bool createDirectories, CancellationToken cancellationToken = default);
+ public static Task WriteAllTextAsync(string path, string? contents, bool createDirectories, CancellationToken cancellationToken = default);
}
Any method that involves creation of a file are included in the list.
Open questions
- Should
Openmethods get included in this? Depending on theFileModeit may not end up creating a new file. - Should
OpenHandlebe included in this? The use of file handles IMO falls into more of an advanced API. CreateSymbolicLink?Copy/Move/Replace?- Perhaps a better name than
createDirectories(singularcreateDirectory?)
API Usage
// string filePath = ...
// Previously:
(new FileInfo(filePath)).Directory.Create()
File.WriteAllText(filePath, text)
// Alternatively:
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
File.WriteAllText(filePath, text)
// Simplified, per proposal
File.WriteAllText(filePath, text, createDirectories: true)
Alternative Designs
None from what I can think of currently...
Risks
- Could it be possible that some sort of path traversal attack is made possible with improper use of this API?
- The operation is not going to be atomic. There is no OS that supports this in one go, so it would always have to be a two-step process involving creating the directories then creating the file
- If called repeatedly against the same parent directory, there would be unnecessary attempts to create the parent directory (but IMO this is more of an improper use than something fundamentally wrong with the design)
- What to do if we successfully create the directory but somehow fail creating the file?
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/runtime
-
agentic-workflows untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
area-System.Reflection blocking-clean-ci-optional Known Build Error os-mac-os-x untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
area-CodeGen-coreclr untriaged
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
agentic-workflows untriaged
Difficulty 1/5 Under an hour Newbie friendliness 78/100
-
area-VM-meta-mono untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
:watch: Not Triaged 11.0 fundamentals/subsvc
Difficulty 2/5 1-3 hours Newbie friendliness 92/100
dotnet/AspNetCore.Docs#37699 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
SubtitleEdit/subtitleedit#15108 · 1 comment ·
-
area/docs-content Bug pulumi/docs
Difficulty 1/5 1-3 hours Newbie friendliness 94/100
-
Create parent directories only after the containment check in InstallHelper.TryExtractToDirectory Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
PowerShell/PSResourceGet#2056 ·