C# Files and Directories
Persist program data, build paths safely, choose whole-file or streaming APIs, and handle external storage as an unreliable boundary.
Before this lesson
- Strings
- Exceptions and using
- Collections
Read and write small text files
Build paths without manual separators
Choose convenience APIs or streams from file size and control needs
The short answer
System.IO provides File, Directory, Path, and stream APIs. Use File convenience methods for small whole-file operations, streams for incremental control or large data, Path.Combine for paths, and explicit exception handling at the application boundary.
Memory disappears; files persist
Variables and collections hold program state while the process runs. When it ends, that in-memory state is gone unless the program writes it somewhere durable. A text file is the simplest persistence boundary and helps reveal issues that do not exist with hardcoded data: paths, encoding, permissions, partial writes, and malformed content.
A file is bytes with a name in a filesystem. Text APIs encode characters into bytes and decode them back. Modern .NET convenience methods use UTF-8 defaults appropriate for much text, but interoperability may require an explicit encoding agreed with another system.
Start with whole-file convenience methods
File.WriteAllText replaces a file with one string, AppendAllText adds text, WriteAllLines writes a sequence of lines, and matching Read methods retrieve content. These methods open, use, and close the file internally, making them a good fit for small settings, exports, and learning projects.
Replacing and appending are different data policies. An activity log usually appends; a generated report may replace; a critical settings file may require writing a temporary file and atomically replacing the old one. Choose deliberately rather than using the first method autocomplete suggests.
string path = "visits.txt";
File.AppendAllText(path, DateTime.UtcNow + Environment.NewLine);
string content = File.ReadAllText(path);
Console.WriteLine(content);Build paths as paths
Hardcoding separators such as \ or / makes platform assumptions and creates doubled or missing separators. Path.Combine joins segments according to the current platform. AppContext.BaseDirectory locates the application base, while Environment.SpecialFolder APIs identify user locations when that is truly where data belongs.
Never combine untrusted text into a sensitive base path and assume it stays inside that directory. Inputs containing rooted paths or parent traversal segments can escape the intended location. Validate allowed file names, resolve the full path, and confirm it remains within the approved root for real upload or file-serving features.
string folder = Path.Combine("data", "reports");
Directory.CreateDirectory(folder);
string reportPath = Path.Combine(folder, "weekly.txt");
File.WriteAllText(reportPath, "Completed: 7");Use streams when the whole file should not be in memory
ReadAllText loads an entire file. For a large log, streaming one line at a time uses bounded memory and lets processing begin before the end is read. StreamReader and StreamWriter expose that control and must be disposed with using.
File APIs face external change. A file can disappear after File.Exists returns true, so existence checks do not replace handling the operation's exceptions. Catch specific failures where the program can offer recovery, and do not reveal internal server paths to untrusted users.
| Need | Prefer | Tradeoff |
|---|---|---|
| Small complete text | ReadAllText / WriteAllText | Simple; loads all content |
| Small line collection | ReadAllLines / WriteAllLines | Convenient array; loads all lines |
| Large or incremental text | StreamReader / StreamWriter | More control and cleanup |
| Structured application data | Serialization plus schema/model | Adds format and compatibility decisions |
using (StreamReader reader = File.OpenText("events.txt"))
{
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
}Good habits
- Use Path.Combine instead of manual separators.
- Keep file I/O outside calculation methods when possible.
- Validate untrusted names and contain resolved paths inside an allowed root.
- Decide overwrite, append, backup, and encoding policies explicitly.
Quick knowledge check
Answer before you reveal.
01Does File.Exists guarantee the later read will succeed?
No. The file or permissions can change between operations. Handle failures from the actual read.
02When is ReadAllText a poor choice?
When a file may be too large to load comfortably or processing should be incremental.
Practice challenge
Now build it without copying.
Create a journal program that appends a timestamped line, reads all entries, and prints how many entries exist. Use a dedicated data directory and Path.Combine.
You are done when
- Running twice preserves the first entry
- The directory is created if absent
- Path separators are not hardcoded
- Missing or inaccessible storage produces a useful boundary message
Stretch: Stream the entries and print only lines containing a search term without loading the entire file.
Open challenge in playgroundLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
Where should an application save files?
It depends on the application, platform, deployment, permissions, and whether data belongs to a user or service. Avoid assuming the current working directory is permanent writable storage.
Should I store complex data as CSV or JSON?
Choose a format based on structure and consumers. CSV fits tabular data with careful escaping; JSON fits nested structured data. Use established parsers and serializers rather than hand-building either format.
What is a stream?
It is an abstraction for reading or writing a sequence of bytes over time, whether the source is a file, memory, network connection, or another provider.