Free Ebook cover C# Essentials: Building Console Apps with .NET

C# Essentials: Building Console Apps with .NET

New course

12 pages

Working with Files in .NET: Reading and Writing Data

Capítulo 8

Estimated reading time: 9 minutes

+ Exercise

Why File I/O Matters in Console Apps

Console apps often need to persist data between runs: saving user preferences, caching results, exporting reports, or keeping a simple log. In .NET, the System.IO namespace provides straightforward APIs for reading and writing files. This chapter focuses on safe, practical scenarios using the static File class for common tasks.

Working Directory, Relative Paths, and Absolute Paths

What is the working directory?

When you use a relative path (like data.txt or data\notes.txt), .NET resolves it relative to the process working directory (also called the current directory). In a console app, this is commonly the folder where the app is launched from (often the build output folder when running from an IDE, but it can differ when launched from a terminal).

You can inspect it at runtime:

using System;using System.IO;Console.WriteLine("Current directory: " + Directory.GetCurrentDirectory());

Relative paths

Relative paths are short and convenient, but they can be confusing if the working directory changes. Examples:

  • data.txt (in the current directory)
  • data\data.txt (in a subfolder named data)
  • ..\data.txt (one folder up)

Absolute paths

Absolute paths fully specify the location. Examples:

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

  • Windows: C:\Users\Ava\Documents\data.txt
  • macOS/Linux: /Users/ava/Documents/data.txt

Absolute paths are explicit and predictable, but less portable across machines.

Making paths explicit (recommended)

A practical compromise is to build an explicit path based on a known base directory. Two common choices are:

  • Current directory (explicitly retrieved): Directory.GetCurrentDirectory()
  • App base directory (where the executable is located): AppContext.BaseDirectory

Use Path.Combine to join path segments safely across operating systems:

using System;using System.IO;string baseDir = AppContext.BaseDirectory;string filePath = Path.Combine(baseDir, "data", "notes.txt");Console.WriteLine(filePath);

If you write to a subfolder, ensure it exists:

using System.IO;string folderPath = Path.Combine(AppContext.BaseDirectory, "data");Directory.CreateDirectory(folderPath);

Reading Files

Reading the entire file as a single string: File.ReadAllText

File.ReadAllText loads the whole file into memory as one string. It’s ideal for small configuration files, templates, or saved text.

using System.IO;string text = File.ReadAllText("data.txt");Console.WriteLine(text);

Notes:

  • If the file doesn’t exist, it throws FileNotFoundException.
  • If the path is invalid, it can throw ArgumentException or NotSupportedException.
  • If access is denied, it can throw UnauthorizedAccessException.

Reading lines into an array: File.ReadAllLines

File.ReadAllLines reads a text file and returns a string[], one element per line. This is convenient for simple line-based formats (lists, logs, one-record-per-line data).

using System.IO;string[] lines = File.ReadAllLines("data.txt");foreach (string line in lines){    Console.WriteLine("- " + line);}

Writing Files

Overwriting (or creating) a file: File.WriteAllText

File.WriteAllText creates a file if it doesn’t exist, or overwrites it if it does. Use it when you want the file to represent the latest state.

using System.IO;File.WriteAllText("data.txt", "Hello from .NET!");

Appending to a file: File.AppendAllText

File.AppendAllText adds text to the end of a file (and creates it if missing). It’s useful for logs or accumulating entries.

using System.IO;File.AppendAllText("log.txt", "Started at: " + DateTime.Now + Environment.NewLine);

Environment.NewLine inserts the correct line ending for the current OS.

Handling Missing Files and Invalid Paths Safely

File I/O is inherently error-prone: files may not exist, paths may be malformed, or permissions may block access. For console apps, aim for clear messages and safe fallbacks.

Step 1: Validate and build a safe path

Prefer building paths with Path.Combine and avoid letting raw user input become a full path without checks. If you accept a filename from the user, treat it as a name (not a path), and store it in a known folder.

using System;using System.IO;string folder = Path.Combine(AppContext.BaseDirectory, "data");Directory.CreateDirectory(folder);Console.Write("Enter a file name (no path): ");string fileName = Console.ReadLine() ?? "";foreach (char c in Path.GetInvalidFileNameChars()){    fileName = fileName.Replace(c.ToString(), "_");}if (string.IsNullOrWhiteSpace(fileName)) fileName = "default.txt";if (!fileName.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) fileName += ".txt";string path = Path.Combine(folder, fileName);Console.WriteLine("Will use: " + path);

Step 2: Check existence when reading

Before reading, you can check File.Exists and decide what to do if it’s missing.

using System.IO;string path = "data.txt";if (!File.Exists(path)){    Console.WriteLine("No saved file found yet.");}else{    string text = File.ReadAllText(path);    Console.WriteLine(text);}

Step 3: Use structured exception handling for I/O

Even with checks, errors can still happen (race conditions, permissions, locked files). Catch specific exceptions and provide actionable output.

using System;using System.IO;string path = "data.txt";try{    string text = File.ReadAllText(path);    Console.WriteLine(text);}catch (FileNotFoundException){    Console.WriteLine("The file was not found: " + path);}catch (DirectoryNotFoundException){    Console.WriteLine("The folder in the path does not exist.");}catch (UnauthorizedAccessException){    Console.WriteLine("Access denied. Try a different folder or run with appropriate permissions.");}catch (ArgumentException){    Console.WriteLine("The path is invalid.");}catch (IOException ex){    Console.WriteLine("An I/O error occurred: " + ex.Message);}

For writing, similar exceptions can occur. If you write to a subfolder, create it first with Directory.CreateDirectory.

Exercise: Save User-Entered Data, Load It Next Run, Display Results

Goal: Build a small console app that stores a list of user-entered items in a text file. On startup, it loads existing items (if any) and displays them. Then it asks for new items, saves them, and exits.

Rules for the exercise

  • Store the file in a known location relative to the app: a data folder under AppContext.BaseDirectory.
  • Use one item per line in the file.
  • Use File.ReadAllLines to load and File.WriteAllText to save (or AppendAllText if you choose to append).
  • Handle missing file and common path/permission errors.

Step-by-step implementation

Step 1: Decide where the file will be created

We’ll create data/items.txt under the app base directory. This makes the location explicit and consistent.

using System;using System.IO;string dataFolder = Path.Combine(AppContext.BaseDirectory, "data");Directory.CreateDirectory(dataFolder);string filePath = Path.Combine(dataFolder, "items.txt");Console.WriteLine("Data file: " + filePath);

Step 2: Load existing items (if the file exists)

using System;using System.IO;string[] existingItems = Array.Empty<string>();try{    if (File.Exists(filePath)){        existingItems = File.ReadAllLines(filePath);    }}catch (UnauthorizedAccessException){    Console.WriteLine("Cannot read the data file due to permissions.");}catch (IOException ex){    Console.WriteLine("Could not read the data file: " + ex.Message);}

Step 3: Display what was loaded

using System;Console.WriteLine();Console.WriteLine("Saved items:");if (existingItems.Length == 0){    Console.WriteLine("(none yet)");}else{    for (int i = 0; i < existingItems.Length; i++){        Console.WriteLine($"{i + 1}. {existingItems[i]}");    }}

Step 4: Collect new items from the user

Let the user enter items until they submit an empty line.

using System;var newItems = new System.Collections.Generic.List<string>();Console.WriteLine();Console.WriteLine("Enter new items (blank line to finish):");while (true){    Console.Write("> ");    string input = Console.ReadLine() ?? "";    if (string.IsNullOrWhiteSpace(input)) break;    newItems.Add(input.Trim());}

Step 5: Save the combined list back to the file

We’ll overwrite the file with the updated list to keep it simple and deterministic.

using System;using System.IO;var allItems = new System.Collections.Generic.List<string>();allItems.AddRange(existingItems);allItems.AddRange(newItems);try{    string output = string.Join(Environment.NewLine, allItems);    File.WriteAllText(filePath, output);    Console.WriteLine();    Console.WriteLine("Saved " + allItems.Count + " item(s).");}catch (UnauthorizedAccessException){    Console.WriteLine("Cannot write the data file due to permissions.");}catch (DirectoryNotFoundException){    Console.WriteLine("The data folder was not found.");}catch (IOException ex){    Console.WriteLine("Could not write the data file: " + ex.Message);}

Full example (put into Program.cs)

using System;using System.IO;using System.Collections.Generic;string dataFolder = Path.Combine(AppContext.BaseDirectory, "data");Directory.CreateDirectory(dataFolder);string filePath = Path.Combine(dataFolder, "items.txt");Console.WriteLine("Data file: " + filePath);string[] existingItems = Array.Empty<string>();try{    if (File.Exists(filePath)){        existingItems = File.ReadAllLines(filePath);    }}catch (UnauthorizedAccessException){    Console.WriteLine("Cannot read the data file due to permissions.");}catch (IOException ex){    Console.WriteLine("Could not read the data file: " + ex.Message);}Console.WriteLine();Console.WriteLine("Saved items:");if (existingItems.Length == 0){    Console.WriteLine("(none yet)");}else{    for (int i = 0; i < existingItems.Length; i++){        Console.WriteLine($"{i + 1}. {existingItems[i]}");    }}var newItems = new List<string>();Console.WriteLine();Console.WriteLine("Enter new items (blank line to finish):");while (true){    Console.Write("> ");    string input = Console.ReadLine() ?? "";    if (string.IsNullOrWhiteSpace(input)) break;    newItems.Add(input.Trim());}var allItems = new List<string>();allItems.AddRange(existingItems);allItems.AddRange(newItems);try{    string output = string.Join(Environment.NewLine, allItems);    File.WriteAllText(filePath, output);    Console.WriteLine();    Console.WriteLine("Saved " + allItems.Count + " item(s).");}catch (UnauthorizedAccessException){    Console.WriteLine("Cannot write the data file due to permissions.");}catch (DirectoryNotFoundException){    Console.WriteLine("The data folder was not found.");}catch (IOException ex){    Console.WriteLine("Could not write the data file: " + ex.Message);}

Common Pitfalls and Practical Tips

Know where your file is being created

If you use a relative path like items.txt, it will be created in the current working directory, which may not be the same as the executable folder. Printing Directory.GetCurrentDirectory() and printing the full file path you’re using are simple debugging tools.

Prefer Path.Combine over manual separators

Use Path.Combine instead of hardcoding \ or /. It avoids subtle bugs and improves portability.

Choose overwrite vs append intentionally

  • Use WriteAllText when the file should match your current state.
  • Use AppendAllText when you want a running history (like a log).

Handle errors with clear messages

In a console app, the best user experience is usually: explain what failed, show the path involved, and suggest a next step (choose a different folder, check permissions, create the missing directory).

Now answer the exercise about the content:

In a .NET console app, what is the recommended way to create a predictable file path for data stored in a subfolder next to the executable?

You are right! Congratulations, now go to the next page

You missed! Try again.

Building the path from AppContext.BaseDirectory with Path.Combine makes the location explicit and cross-platform. Creating the folder with Directory.CreateDirectory prevents errors when writing to a subfolder.

Next chapter

Basic Error Handling and Defensive Coding in C#

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.