Your notebook has a catch: when you quit it, all your notes are gone. The list lives in memory, and the operating system frees that memory as soon as the program ends. Whatever should stay has to go into a file. In this last lesson your notebook learns exactly that, and then it’s done.
Writing to a file and reading from it
.NET comes with File, a building block for files. For lines of text, three of its methods are all you need:
// files.cs
List<string> lines = ["first line", "second line"];
File.WriteAllLines("test.txt", lines);
Console.WriteLine($"Does test.txt exist? {File.Exists("test.txt")}");
string[] fromFile = File.ReadAllLines("test.txt");
Console.WriteLine($"{fromFile.Length} lines read:");
foreach (string line in fromFile)
{
Console.WriteLine(line);
}
Does test.txt exist? True
2 lines read:
first line
second line
File.WriteAllLines(name, list)writes each entry into the file as a line of its own. If the file already exists, it gets overwritten.File.Exists(name)tells you whether there’s a file with that name.File.ReadAllLines(name)reads all the lines. It returns an array,string[], which you know from lesson 7. The length of an array is calledLength, notCount. If the file doesn’t exist,ReadAllLinesstops with an exception. That’s why you ask withExistsfirst.
Where is test.txt now? In the folder where you started the program, that is, the current folder of your terminal. Take a look with
ls. The file is plain text; go ahead and open it in your editor.
The notebook remembers everything
Two new methods are enough. Load reads the notes at the start if the file already exists, and otherwise returns an empty list:
// excerpt from notebook.cs
static List<string> Load(string file)
{
List<string> notes = [];
if (File.Exists(file))
{
foreach (string line in File.ReadAllLines(file))
{
notes.Add(line);
}
}
return notes;
}
Save writes the whole list to the file:
// excerpt from notebook.cs
static void Save(string file, List<string> notes)
{
File.WriteAllLines(file, notes);
}
The main program loads the notes at the start and saves after every change, that is, after menu items 1 and 4. With a few hundred notes
that takes no noticeable time, and it has a big advantage: if someone closes the terminal window instead of quitting with 0, nothing is
lost. The main program calls Save, not AddNote or Delete. That way each method keeps its one job: some change the list, the other
writes it out.
The file name appears in only one place, the variable file, and gets passed to the methods. This is your finished notebook:
// notebook.cs
string file = "notes.txt";
List<string> notes = Load(file);
Console.WriteLine($"Your notebook. Saved notes: {notes.Count}");
bool running = true;
while (running)
{
ShowMenu();
string choice = (Console.ReadLine() ?? "0").Trim();
if (choice == "1")
{
AddNote(notes);
Save(file, notes);
}
else if (choice == "2")
{
ShowAll(notes);
}
else if (choice == "3")
{
Search(notes);
}
else if (choice == "4")
{
Delete(notes);
Save(file, notes);
}
else if (choice == "0")
{
running = false;
}
else
{
Console.WriteLine("There is no such choice.");
}
}
Console.WriteLine("See you soon!");
static void ShowMenu()
{
Console.WriteLine();
Console.WriteLine("1 New note");
Console.WriteLine("2 Show all notes");
Console.WriteLine("3 Search");
Console.WriteLine("4 Delete a note");
Console.WriteLine("0 Quit");
Console.Write("Your choice: ");
}
static void AddNote(List<string> notes)
{
Console.Write("Note: ");
string text = (Console.ReadLine() ?? "").Trim();
if (text == "")
{
Console.WriteLine("I won't add an empty note.");
return;
}
notes.Add(text);
Console.WriteLine($"Noted. Number of notes: {notes.Count}");
}
static void ShowAll(List<string> notes)
{
if (notes.Count == 0)
{
Console.WriteLine("No notes yet.");
return;
}
for (int i = 0; i < notes.Count; i++)
{
Console.WriteLine($"{i + 1}. {notes[i]}");
}
}
static void Search(List<string> notes)
{
Console.Write("Search term: ");
string searchTerm = (Console.ReadLine() ?? "").Trim();
if (searchTerm == "")
{
Console.WriteLine("I can't search without a search term.");
return;
}
int matches = 0;
for (int i = 0; i < notes.Count; i++)
{
if (notes[i].Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"{i + 1}. {notes[i]}");
matches++;
}
}
Console.WriteLine($"Matches: {matches}");
}
static void Delete(List<string> notes)
{
ShowAll(notes);
if (notes.Count == 0)
{
return;
}
Console.Write("Delete which number? ");
string input = Console.ReadLine() ?? "";
if (!int.TryParse(input, out int number) || number < 1 || number > notes.Count)
{
Console.WriteLine($"Please enter a number from 1 to {notes.Count}.");
return;
}
string text = notes[number - 1];
notes.RemoveAt(number - 1);
Console.WriteLine($"Deleted: {text}");
}
static List<string> Load(string file)
{
List<string> notes = [];
if (File.Exists(file))
{
foreach (string line in File.ReadAllLines(file))
{
notes.Add(line);
}
}
return notes;
}
static void Save(string file, List<string> notes)
{
File.WriteAllLines(file, notes);
}
Run it, add two notes and quit with 0. Then run it a second time:
Your notebook. Saved notes: 2
1 New note
2 Show all notes
3 Search
4 Delete a note
0 Quit
Your choice: 2
1. Dentist on Tuesday
2. Buy bread
Your notes survived the end of the program. They’re in notes.txt in the same folder, one note per line. A word of advice that holds
for every program: don’t put anything secret in there, no passwords, no PINs. The file isn’t encrypted, and anyone who gets to your
computer can read it. Secrets never belong in code, never in log files and never in plain text files.
Testing like an inspector
Your notebook is done when it holds up in the awkward cases. Go through this whole list once and check off each item:
- First start without
notes.txt: “Saved notes: 0”, and choosing 2 shows “No notes yet.” - Add two notes, quit with 0, start again: both are there, in the same order.
- Add a note and close the terminal window without choosing 0. Open a new terminal and start again: the note is there.
- Search with different uppercase and lowercase letters than in the note: it’s found.
- Delete with 0, with a number that’s too high and with a word: a hint each time, nothing deleted.
- Delete the last note and start again: “Saved notes: 0”.
- A note with accented letters, such as “Café with Zoë”: it’s saved correctly and shown correctly after the restart.
If all seven points check out, you’ve written a program people can actually use. Congratulations!
Exercise 1
Right now deleting is very quick, even by accident. Change Delete so it asks before removing anything:
Really delete "Buy bread"? (y/n). It deletes only for “y” or “yes”, whether typed in uppercase or lowercase. Otherwise the method
reports “Nothing deleted.”
Solution 1
Replace the method Delete with this one. The lines after string text = notes[number - 1]; are new:
// excerpt from notebook.cs
static void Delete(List<string> notes)
{
ShowAll(notes);
if (notes.Count == 0)
{
return;
}
Console.Write("Delete which number? ");
string input = Console.ReadLine() ?? "";
if (!int.TryParse(input, out int number) || number < 1 || number > notes.Count)
{
Console.WriteLine($"Please enter a number from 1 to {notes.Count}.");
return;
}
string text = notes[number - 1];
Console.Write($"Really delete \"{text}\"? (y/n) ");
string answer = (Console.ReadLine() ?? "").Trim().ToLower();
if (answer != "y" && answer != "yes")
{
Console.WriteLine("Nothing deleted.");
return;
}
notes.RemoveAt(number - 1);
Console.WriteLine($"Deleted: {text}");
}
The question follows the same pattern as the checks before it: first the case where nothing should happen, with return, then the
actual work. Look closely at the condition answer != "y" && answer != "yes". In words: “neither y nor yes”. With || it would always
be true, because every answer differs from at least one of the two. That’s a mistake almost everyone makes once.
Exercise 2
Open notes.txt in your editor, insert an empty line between two notes and save. Start the notebook and have it show all notes. What do
you see? Change Load so it skips empty lines and lines made of nothing but spaces.
Solution 2
Before the change, the empty line shows up as a note of its own: a number with a period after it and nothing else. The notebook itself
never adds empty notes, but anyone can change a file. That’s why a program should always check data that comes from outside. This is how
Load skips the empty lines:
// excerpt from notebook.cs
static List<string> Load(string file)
{
List<string> notes = [];
if (File.Exists(file))
{
foreach (string line in File.ReadAllLines(file))
{
if (line.Trim() != "")
{
notes.Add(line.Trim());
}
}
}
return notes;
}
line.Trim() removes the spaces at the edges. If nothing is left after that, the line was empty. The line is stored without the spaces
at its edges, just as AddNote does with new notes.
Exercise 3
As a bonus: a menu item 5 Edit a note. It shows all notes, asks for the number and for the new text, and replaces the note. An empty
new text changes nothing. Tip: notes[number - 1] = text; replaces an entry in the list.
Solution 3
The new method Edit reads the number just like Delete. Here’s the whole notebook with all three extensions from this lesson:
// notebook.cs
string file = "notes.txt";
List<string> notes = Load(file);
Console.WriteLine($"Your notebook. Saved notes: {notes.Count}");
bool running = true;
while (running)
{
ShowMenu();
string choice = (Console.ReadLine() ?? "0").Trim();
if (choice == "1")
{
AddNote(notes);
Save(file, notes);
}
else if (choice == "2")
{
ShowAll(notes);
}
else if (choice == "3")
{
Search(notes);
}
else if (choice == "4")
{
Delete(notes);
Save(file, notes);
}
else if (choice == "5")
{
Edit(notes);
Save(file, notes);
}
else if (choice == "0")
{
running = false;
}
else
{
Console.WriteLine("There is no such choice.");
}
}
Console.WriteLine("See you soon!");
static void ShowMenu()
{
Console.WriteLine();
Console.WriteLine("1 New note");
Console.WriteLine("2 Show all notes");
Console.WriteLine("3 Search");
Console.WriteLine("4 Delete a note");
Console.WriteLine("5 Edit a note");
Console.WriteLine("0 Quit");
Console.Write("Your choice: ");
}
static void AddNote(List<string> notes)
{
Console.Write("Note: ");
string text = (Console.ReadLine() ?? "").Trim();
if (text == "")
{
Console.WriteLine("I won't add an empty note.");
return;
}
notes.Add(text);
Console.WriteLine($"Noted. Number of notes: {notes.Count}");
}
static void ShowAll(List<string> notes)
{
if (notes.Count == 0)
{
Console.WriteLine("No notes yet.");
return;
}
for (int i = 0; i < notes.Count; i++)
{
Console.WriteLine($"{i + 1}. {notes[i]}");
}
}
static void Search(List<string> notes)
{
Console.Write("Search term: ");
string searchTerm = (Console.ReadLine() ?? "").Trim();
if (searchTerm == "")
{
Console.WriteLine("I can't search without a search term.");
return;
}
int matches = 0;
for (int i = 0; i < notes.Count; i++)
{
if (notes[i].Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"{i + 1}. {notes[i]}");
matches++;
}
}
Console.WriteLine($"Matches: {matches}");
}
static void Delete(List<string> notes)
{
ShowAll(notes);
if (notes.Count == 0)
{
return;
}
Console.Write("Delete which number? ");
string input = Console.ReadLine() ?? "";
if (!int.TryParse(input, out int number) || number < 1 || number > notes.Count)
{
Console.WriteLine($"Please enter a number from 1 to {notes.Count}.");
return;
}
string text = notes[number - 1];
Console.Write($"Really delete \"{text}\"? (y/n) ");
string answer = (Console.ReadLine() ?? "").Trim().ToLower();
if (answer != "y" && answer != "yes")
{
Console.WriteLine("Nothing deleted.");
return;
}
notes.RemoveAt(number - 1);
Console.WriteLine($"Deleted: {text}");
}
static void Edit(List<string> notes)
{
ShowAll(notes);
if (notes.Count == 0)
{
return;
}
Console.Write("Edit which number? ");
string input = Console.ReadLine() ?? "";
if (!int.TryParse(input, out int number) || number < 1 || number > notes.Count)
{
Console.WriteLine($"Please enter a number from 1 to {notes.Count}.");
return;
}
Console.Write("New text: ");
string text = (Console.ReadLine() ?? "").Trim();
if (text == "")
{
Console.WriteLine("I won't leave a note empty. To remove one, choose 4.");
return;
}
notes[number - 1] = text;
Console.WriteLine($"Edited: {text}");
}
static List<string> Load(string file)
{
List<string> notes = [];
if (File.Exists(file))
{
foreach (string line in File.ReadAllLines(file))
{
if (line.Trim() != "")
{
notes.Add(line.Trim());
}
}
}
return notes;
}
static void Save(string file, List<string> notes)
{
File.WriteAllLines(file, notes);
}
Delete and Edit now read the number with almost the same lines; only the question is different. Duplicated code calls for a method,
as in lesson 8. If you feel like it, write a method static int ReadNoteNumber(List<string> notes, string question) that returns a
valid number, or 0 if the input doesn’t fit, and use it in both methods. Then use the test list from above to check that everything
still works exactly as before.
What you can do now
- You write lines of text to a file with
File.WriteAllLinesand read them withFile.ReadAllLines. - You check with
File.Existswhether a file is there before you read it. - You know that a file with a plain name ends up in the current folder, and that secrets don’t belong in text files.
- You check data that comes from outside.
- You’ve written a complete program that people can use, and tested it systematically.
Where to go from here
First, take the self-test that belongs to this course. It takes about half an hour and shows you what has stuck.
After that you have several options:
- Practice. Extend your notebook with ideas of your own: move a note to the top, delete all notes after asking first, have the search count its matches. Every idea of your own that you build teaches you more than any page you read.
- Keep learning with Microsoft Learn. The learning paths “Get started with C#”, parts 2 to 6, go deeper into what you started here, with exercises and projects of their own.
- The academy’s follow-up course turns the notebook into the Kladde with contacts and appointments: with classes, saving as JSON, automated tests and Git, just like on the job. If you’d like to hear when it starts, put your name on the waiting list on the academy’s page.
Read more
- Build file-based apps: what programs made of a single file can also do, such as reading arguments and using packages.
- Work with files and directories in a .NET app: a module about files and folders.
- Create and run simple C# console applications (Get started with C#, Part 2): the second learning path in the “Get started with C#” series, which continues up to part 6.