Working with Files
Overview
File operations live on IAbsoluteFilePath. Relative file paths can describe a file but cannot perform I/O; combine them with an absolute directory first (see Combining and Navigating Paths).
Every method described here can throw IOException (or one of its subtypes: FileNotFoundException, DirectoryNotFoundException, UnauthorizedIOAccessException, etc.). See Exception Handling for patterns.
Existence Checks
Exists
Exists gives a quick boolean:
if (file.Exists)
file.Delete();
State
For richer information, use State. It returns one of four EntryState values:
| Value | Meaning |
|---|---|
| Exists | The file exists. |
| ParentExists | The file does not exist, but its parent directory does. |
| ParentDoesNotExist | Neither the file nor its parent directory exist. |
| WrongType | A different kind of entry (e.g. a directory) exists at this path. |
switch (file.State)
{
case EntryState.Exists: /* open and read */ break;
case EntryState.ParentExists: /* prompt user, can create */ break;
case EntryState.ParentDoesNotExist: /* would need to create the directory tree */ break;
case EntryState.WrongType: /* a directory is here: refuse */ break;
}
Tip
Use State over Exists when your error handling needs to distinguish between "missing" and "wrong type". WrongType in particular catches a class of bugs that System.IO's boolean checks silently obscure.
Opening Streams
OpenStream
OpenStream has the following signature:
FileStream OpenStream(
FileMode mode = FileMode.Open,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None,
int bufferSize = 4096,
FileOptions options = FileOptions.None);
Always wrap the returned FileStream in using:
using FileStream stream = configFile.OpenStream(FileMode.Open, FileAccess.Read, FileShare.Read);
using StreamReader reader = new(stream);
string text = reader.ReadToEnd();
For writes, choose the appropriate FileMode:
| FileMode | Behavior |
|---|---|
| Open | File must exist; throws otherwise. |
| OpenOrCreate | Open if exists, create if not. |
| Create | Always create (truncates if exists). |
| CreateNew | Create; throw if exists. |
| Append | Open for append (write only); create if missing. |
| Truncate | Open and truncate; file must exist. |
OpenAsyncStream
OpenAsyncStream has an identical signature to OpenStream but always sets Asynchronous:
using FileStream stream = file.OpenAsyncStream(FileMode.Create, FileAccess.Write);
await stream.WriteAsync(buffer);
Note
The OS may not actually support asynchronous I/O for the underlying handle, in which case the runtime falls back to synchronous internally. The Asynchronous option only opts in to true async when the platform allows it.
File Properties
Length and IsReadOnly
Use Length and IsReadOnly:
long bytes = file.Length;
bool readOnly = file.IsReadOnly;
file.IsReadOnly = true; // toggle the read-only attribute
Attributes and Timestamps
Inherited from IAbsolutePath:
file.Attributes; // FileAttributes
file.CreationTime; // local time
file.CreationTimeUtc;
file.LastAccessTime;
file.LastWriteTime;
file.LastWriteTimeUtc;
file.Attributes |= FileAttributes.Hidden;
file.LastWriteTimeUtc = DateTime.UtcNow;
Tip
Each attribute/timestamp access touches the file system. If you'll read several at once, prefer GetInfo which fetches everything in a single call. See Cached Entry Info.
Copy, Move, Replace
CopyTo
Use CopyTo:
file.CopyTo(destination); // throws if destination exists
file.CopyTo(destination, overwrite: true);
MoveTo
Use MoveTo:
file.MoveTo(destination);
file.MoveTo(destination, overwrite: true);
Note
MoveTo and CopyTo accept any IAbsoluteFilePath as the destination, including across directories or drives. The destination's parent must already exist; call Create on ParentDirectory first if needed.
Replace
Replace atomically replaces the contents of an existing file with this file, optionally producing a backup of the replaced file:
newFile.Replace(originalFile, backupFile: null);
newFile.Replace(originalFile, backupFile: backupPath, ignoreMetadataErrors: true);
Replace is the recommended way to atomically commit changes: write to a temporary file, then call it to swap the temp file into place.
Deleting
Use Delete:
file.Delete(); // ignores not-found by default
file.Delete(ignoreNotFound: false); // throws FileNotFoundException if absent
Putting It Together
Atomic write pattern:
IAbsoluteFilePath finalPath = appBase.CombineFile("config/app.json");
IAbsoluteFilePath tempPath = finalPath.AddExtension(".tmp");
finalPath.ParentDirectory.Create();
using (FileStream s = tempPath.OpenStream(FileMode.Create, FileAccess.Write))
using (StreamWriter w = new(s))
{
w.Write(serializedJson);
}
if (finalPath.Exists)
tempPath.Replace(finalPath, backupFile: null);
else
tempPath.MoveTo(finalPath);
Cached Snapshots
If you want a single consistent view of a file's metadata (size, attributes, timestamps), call GetInfo to obtain a CachedFileInfo. See Cached Entry Info.
Next Steps
- Working with Directories: many file workflows start by creating a directory.
- Searching and Enumeration: find the files you want to operate on.
- Exception Handling: catch I/O errors cleanly.