Reading and Writing Files

Read and write whole files with the File class, and build paths with Path.Combine so the same code works on every operating system.

Syntaxstring text = File.ReadAllText(path);

A file is a named block of data that stays on disk after your program ends. The types that read and write files live in the System.IO namespace. A namespace is a group of related types; writing using System.IO; at the top of a file lets you name those types without the namespace in front.

Whole-file calls

File.ReadAllText(path) opens the file, reads every character into one string, and closes the file. File.WriteAllText(path, text) creates the file, writes the string, and closes it. If the file already exists it is truncated, so the old contents are gone. File.AppendAllText(path, text) adds to the end instead. File.ReadAllLines(path) returns an array of strings, one per line.

These calls are shorthand. Each one opens a stream, does the work and closes it for you; the next lesson shows that longer form. Use the shorthand whenever the whole file fits comfortably in memory.

Building paths

Never glue path pieces together with +. Windows separates folders with a backslash, Linux and macOS with a forward slash. Path.Combine("data", "notes.txt") inserts the right one for the machine running the code. Path.GetFileName, Path.GetExtension and Path.GetFullPath take a path apart again.

Folders

Directory.CreateDirectory(path) creates every missing folder in the path, and does nothing if they all exist. Directory.EnumerateFiles(path, "*.txt") lists matching files, and the order it returns them in is not guaranteed, so sort if the order matters.

Any of these can throw: FileNotFoundException, DirectoryNotFoundException or UnauthorizedAccessException. Every reading and writing method on File also has an Async twin, such as File.ReadAllTextAsync, which becomes useful once Stage 4 introduces await. The Path and Directory members shown here have no async versions.

Example

Example · csharp
using System;
using System.IO;

// Path.Combine picks the right separator for this operating system.
string folder = Path.Combine("data", "notes");
Directory.CreateDirectory(folder);   // no error if it already exists

string file = Path.Combine(folder, "today.txt");

File.WriteAllText(file, "milk\neggs\n");   // replaces the file if it exists
File.AppendAllText(file, "bread\n");       // adds to the end

string text = File.ReadAllText(file);
Console.Write(text);

string[] lines = File.ReadAllLines(file);
Console.WriteLine($"lines: {lines.Length}");
Console.WriteLine($"name: {Path.GetFileName(file)}");
Console.WriteLine($"exists: {File.Exists(file)}");

// Output:
// milk
// eggs
// bread
// lines: 3
// name: today.txt
// exists: True

When to use it

  • Save a user's settings to a text file next to the program.
  • Read a list of ids from a text file and handle one per line.
  • Create a dated output folder before writing a report into it.
  • Build a file path that works unchanged on Windows and on a Linux container.

More examples

ReadAllLines versus ReadLines

The two methods look alike and behave very differently. ReadAllLines gives you an array you can index and count, which is what you want for a small configuration file. ReadLines reads lazily, one line at a time, so memory use stays flat no matter how big the file is. Reach for ReadLines on log files and data exports.

Example · csharp
using System;
using System.IO;

string path = Path.Combine("data", "ids.txt");
Directory.CreateDirectory("data");
File.WriteAllLines(path, new[] { "17", "4", "23" });

// ReadAllLines builds an array, so every line sits in memory at once.
string[] all = File.ReadAllLines(path);
Console.WriteLine($"count: {all.Length}");

// ReadLines hands back one line at a time, so a huge file still works.
int total = 0;
foreach (string line in File.ReadLines(path))
{
    total += int.Parse(line);
}

Console.WriteLine($"total: {total}");

// Output:
// count: 3
// total: 44

Listing files in a folder

The second argument is a search pattern, where * stands for any run of characters. This is the shape of most batch jobs: find the files that match, then handle each one. Note that the returned strings are full paths, so Path.GetFileName is what you want for display, and Order() keeps the output stable across machines.

Example · csharp
using System;
using System.IO;
using System.Linq;

string folder = Path.Combine("data", "reports");
Directory.CreateDirectory(folder);
File.WriteAllText(Path.Combine(folder, "june.csv"), "x");
File.WriteAllText(Path.Combine(folder, "may.csv"), "x");
File.WriteAllText(Path.Combine(folder, "notes.txt"), "x");

// EnumerateFiles returns full paths, in no guaranteed order, so sort them.
foreach (string path in Directory.EnumerateFiles(folder, "*.csv").Order())
{
    Console.WriteLine(Path.GetFileName(path));
}

// Output:
// june.csv
// may.csv

Replacing a file without leaving it half written

WriteAllText truncates the file first and then writes. If the program is killed in between, the file on disk is empty or partial. Writing to a temporary name and then moving it over the target avoids that, because the move is a single step: a reader sees either the old file or the new one. Use this pattern for anything your program must be able to read back after a crash.

Example · csharp
using System;
using System.IO;

string path = Path.Combine("data", "settings.json");
Directory.CreateDirectory("data");

// A raw string literal (C# 11) holds quotes without backslashes.
File.WriteAllText(path, """{"theme":"dark"}""");

// Write the new content beside the real file, then swap it in.
string temp = path + ".tmp";
File.WriteAllText(temp, """{"theme":"light"}""");
File.Move(temp, path, overwrite: true);

Console.WriteLine(File.ReadAllText(path));
Console.WriteLine(File.Exists(temp));

// Output:
// {"theme":"light"}
// False

Discussion

  • Be the first to comment on this lesson.