Introduction
One very common task in any programming language is the ability to write information to a file and read it back in. This is often called file input and output or simply file I/O.
Writing to files and reading from them is actually a very simple task in C#. There are several ways it can be done. We’ll cover the simplest way here and touch on a few more advanced ways after.
-
RAM (The Workbench): This is where your variables (
int,string,List) live. It is incredibly fast, but it is Volatile. If the program closes or the power cuts out, the workbench is wiped clean. -
HDD/SSD (The Warehouse): This is where Files live. It is slower than RAM, but it is Persistent. Data stored here survives a reboot and can be loaded back months later.
Why We Save to Files:
-
Persistence: To keep player progress (levels, inventory) across different play sessions.
-
Configuration: To remember user preferences (volume, screen resolution) so they don’t have to set them every time.
-
Memory Management: To store huge amounts of data that are too big to fit in the computer’s RAM at once.
Easy Way Writing
we use the File class. It is a “Static” class, meaning you don’t need to create a new version of it; you just call its methods directly to “slurp” or “dump” data.
1.The Single String Method Use this when you have one big block of text ready to go.
string info = "Hello persistent world!";
// Arguments: (Path, Content)
File.WriteAllText("test1.txt", info);
“Wait, I wrote the code, the program finished, but where is my file? My code is lying to me!”
Where is my File?
When you run File.WriteAllText("test1.txt", info);, expect to see it next to Program.cs
When executing a C# application, there is a common point of confusion regarding where files are physically created. To handle files professionally, a developer must understand how the IDE handles the build process.
When you “Run” or “Build” a project in Visual Studio or VS Code, your code is compiled into an assembly (.exe or .dll).
- Source Directory: Where your
.csfiles live. - Output Directory: Usually
bin/Debug/netx.0/.
By default, any relative path used in your code (e.g., File.WriteAllText("data.txt", content)) resolves to the Output Directory, not the folder where your code sits. This is because the Operating System executes the program from the compiled folder.
Absolute vs Relative Path
Absolute Paths
An absolute path provides the full address from the root of the file system
-
Example: `C:\Users\Admin\Project\data.txt
-
Limitation: Non-Portable. If the project is moved to another machine or a different user, the path will be invalid, leading to a
DirectoryNotFoundException.
Relative Paths
A relative path is calculated based on the Current Working Directory of the running application.
-
Example:
data.txtrefers to the same folder as the executable. -
Example:
../logs/log.txtmoves up one directory level before looking for the “logs” folder. -
Advantage: Portable. The application can be moved to any directory or machine as long as the internal folder structure remains the same.
class Program
{
static void Main()
{
string absolutePath = "C:/Dev/Local/dotnet/classic-HelloWorld/HelloWorld/bin/Debug/net10.0/files/absolutePath.txt";
string relativePath = "files/relativePath.txt";
File.WriteAllText(absolutePath, "Saving to an absolute location.");
File.WriteAllText(relativePath, "Saving to a relative location.");
}
}
The Array Method Use this when you have a collection of lines. C# will automatically add a “New Line” after every index.
using System.IO;
// 1. The Ingredients (The Array)
string[] recipe = {
"3x Iron Ingots",
"1x Dragon Scale",
"1x Magic Crystal"
};
// 2. Write them to the Recipe Book!
File.WriteAllLines("MegaSword_Recipe.txt", recipe);
“The Overwrite Trap”
File.WriteAllText(...)
File.WriteAllLines(...)
- It looks for the file.
- If the file already exists, it wipes it completely blank (like erasing a whiteboard).
- It writes your new data into the empty file.
File.AppendAllText(...)
File.AppendAllLines(...)
you want to keep what’s already there and just add to the bottom (like adding a new item to a Minecraft chest), you use the Append versions:
Easy Way Reading
Just like writing, C# gives us two easy ways to grab data:
ReadAllText (The Single String) This pulls the entire file into one massive string.
string rawData = File.ReadAllText("test1.txt");
// Use this if you just need to display a big block of text (like a Credits screen).
Console.WriteLine(rawData);
ReadAllLines (The String Array)
This is much more useful for logic. Every line in the text file becomes an index in a string[].
student_1,1000
student_2,950
student_3,925
student_4,900
student_5,840
string[] lines = File.ReadAllLines("highscores.csv");
// line[0] would be "Kelly,1000"
// Example-1
foreach ( string line in Scores)
{
Console.WriteLine(line);
}
// Example-2
foreach ( string line in Scores)
{
string[] token = line.Split(',');
string name = token[0];
string score = token[1];
Console.WriteLine($"{name} scored {score} points!");
}
Example Project
class Program
{
static void Main()
{
string filePath = "launchCount.txt";
int currentCount = 0;
string savedText = File.ReadAllText(filePath);
currentCount = int.Parse(savedText);
currentCount++;
File.WriteAllText(filePath, currentCount.ToString());
Console.WriteLine($"This app has been opened {currentCount} times!");
}
}
When developing software, how you interact with files significantly impacts performance and system stability. Below is a professional breakdown of the two primary methods for reading data, designed for those new to the field.
1.The Conventional Approach: File.ReadAllLines (Buffer Loading)
This method is a high-level abstraction that reads an entire file into a collection (like an array or list) in one single operation.
-
How it Works: The application requests the entire contents of the file from the disk. The operating system allocates a block of memory (RAM) large enough to hold the entire file, then transfers the data.
-
The Drawback: It is memory-intensive. If a file is 500MB, your application’s memory usage spikes by at least 500MB instantly.
-
The Risk: On systems with limited resources or when handling massive datasets (e.g., multi-gigabyte logs), this approach leads to a
System.OutOfMemoryException, causing the application to crash. -
Efficiency: It is an “all-or-nothing” operation. Even if you only need the first line of a file, the system must process the entire document before you can access any data.
2.The Professional Standard: Streams (Sequential Processing)
A Stream is an abstraction for a sequence of bytes. Instead of treating a file as a single “object” to be moved, streaming treats data as a continuous flow. Processes the file piece by piece. It uses the same tiny amount of memory whether the file is small or massive.
-
How it Works: The application opens a “pipe” to the file. It reads small portions—usually one line or a fixed-size buffer (e.g., 4KB)—into memory, processes it, and then discards it to make room for the next piece.
-
The Primary Benefit: Constant Memory Footprint. Whether the file is 10KB or 10GB, the application uses the same minimal amount of RAM. This makes the code “Memory Safe.”
-
Granular Control: Streams allow for early exit. If you are searching for a specific record and find it at the beginning of the file, you can close the stream immediately, saving both CPU cycles and Disk I/O.
-
Reliability: This method ensures the application remains responsive and stable, regardless of the scale of the external data.
As we discussed, Streams are for when you want to be a professional. Instead of dumping a whole file into RAM, we open a “Pipe” and handle data one bit at a time.
When your data grows beyond a few simple lines, you must move from Bulk Loading to Streaming
Three-Layered Logic To work with streams, you don’t just “open a file.” You build a transport system using three distinct layers
-
The File (The Warehouse): The physical data sitting on the Hard Drive (HDD/SSD).
-
The FileStream (The Pipe): This is the actual connection. It only speaks “Computer” (Bytes/Binary). It moves data fast, but it’s hard for humans to use directly.
-
Raw Movement: The pipe doesn’t care what is moving through it. It could be a photo, a song, or a text file. To the
FileStream, it is all just Bytes. -
Unidirectional vs. Bidirectional: When you open a pipe, you usually tell the computer if you are Reading (pulling from the warehouse) or Writing (pushing to the warehouse).
-
The “Low-Level” Language: If you tried to talk to the pipe directly, you would have to give it numbers. You can’t say “Write ‘Hello’”; you have to say “Write these five bytes: 72, 101, 108, 108, 111.”
-
-
The Helper (The Translator): This is your StreamReader or StreamWriter. It sits at the end of the pipe and translates those raw bytes into human words (
string) or numbers (int).- Since humans don’t speak in bytes, we hire a StreamReader or StreamWriter to sit at our end of the pipe. This is where the magic happens.
- When you tell the
StreamWritertoWriteLine("Hello"), it does three things:- Translates: It looks up the “Dictionary” (Encoding) to see that ‘H’ is 72, ‘e’ is 101, etc.
- Buffers: It doesn’t send those bytes into the pipe one by one. It waits until it has a big enough “bucket” of bytes (a Buffer) before dumping them all into the pipe at once. This makes the program much faster.
- Manages the Pipe: It makes sure the pipe stays open while you’re busy and closes it tightly when you’re done.
- When you ask the
StreamReaderfor aReadLine(), it:- Listens to the Pipe: it grabs the bytes coming through.
- Identifies Boundaries: It looks for the specific byte pattern that means “New Line.”
- Translates Back: It turns those 72s and 101s back into ‘H’ and ‘e’ so you can use them as a
stringvariable in your code.
| Layer | Responsibility | What happens if it’s missing? |
|---|---|---|
| File | Keeping data forever | Your progress is lost when the app closes. |
| FileStream | Moving the data | There is no way to get data into the RAM. |
| Helper | Making it human-readable | You have to do manual binary math for every letter. |
Stream Writer
Level-1 The Raw Pipe
In this stage, we talk directly to the FileStream. This is the “Hard Way” because the pipe doesn’t understand words; it only understands Bytes (numbers).
// 1. Create the physical pipe to the file
FileStream pipe = new FileStream("test.txt", FileMode.Create); //todo why we need filestream, insead what are the thing i can use.
// 2. The computer doesn't know 'H'. It knows the number 72.
// We have to turn our "Hello" into a numeric array (Bytes).
byte[] binaryMessage = { 72, 101, 108, 108, 111 };
// 3. Push those numbers through the pipe
pipe.Write(binaryMessage, 0, binaryMessage.Length);
// 4. Close the pipe manually
pipe.Close();
The Logic: You have to manually convert your message into a list of numbers before the pipe will accept it.
level-2 The Helper Now, we sit a StreamWriter on top of that pipe. This is the “Translator.” You give it a string, and it does the hard math of turning it into bytes for the pipe.
// 1. Create the Pipe (The Infrastructure)
FileStream pipe = new FileStream("test.txt", FileMode.Create);
// 2. Hire the Helper (The Translator)
// We tell the StreamWriter: "Use THIS pipe to send your data."
StreamWriter helper = new StreamWriter(pipe);
// 3. Now we can use easy commands!
helper.WriteLine("This is much easier for humans!");
helper.WriteLine("I can even write numbers directly: " + 500);
// 4. Closing the helper also closes the pipe underneath it.
helper.Close();
The Logic: You talk to the Helper; the Helper talks to the Pipe.
level-2 The Professional way
In the real world, programs crash. If your program crashes at Step 3 (above), the file stays “Locked” and your data might get corrupted. Professionals use the using block to prevent this.
The Logic: The using block is like a Self-Closing Door. The moment the code finishes or crashes, the door slams shut and the file is saved and unlocked automatically.
// 1. Open the Helper and Pipe inside a 'Safety Cage' (using)
using (StreamWriter writer = new StreamWriter("professional_log.txt"))
{
// 2. Do your work
writer.WriteLine("Step 1: System Check...");
writer.WriteLine("Step 2: Saving Progress...");
// You don't need to call .Close()!
// The '}' at the bottom handles it for you.
}
// <--- The Pipe is automatically CLOSED and SAVED exactly here.
Stream Reader
Level-1 The Raw Pipe
When we read with a stream, we use a StreamReader. Instead of a “Loading Dock” for writing, the reader uses a Security Turnstile—it only lets one line into the RAM at a time.
FileStream pipe = new FileStream("test.txt", FileMode.Open);
byte[] buffer = new byte[5];
pipe.Read(buffer, 0, 5); // You get: { 72, 101, 108, 108, 111 }
// You have to manually turn those numbers into "Hello"
pipe.Close();
Just like writing, the raw pipe only gives you numbers. You have to translate them yourself.
Level-2 The Helper
We wrap the pipe in a StreamReader so we can use human commands like ReadLine().
FileStream pipe = new FileStream("test.txt", FileMode.Open);
StreamReader helper = new StreamReader(pipe);
// The helper does the translation for you!
string secret = helper.ReadLine(); // didnt' how the data here.
helper.Close();
Level-3 The Professional Way
This is the most important piece of code for your students. It combines the Safety Cage (using) with a Loop to read a file of any size (1MB or 100GB) using almost zero memory.
// 1. Open the Safety Cage
using (StreamReader reader = new StreamReader("big_data.txt"))
{
string line;
// 2. The "While" Loop: Keep reading as long as there is data
// When ReadLine() hits the end of the file, it returns 'null'
while ((line = reader.ReadLine()) != null)
{
// 3. Process the data one piece at a time
Console.WriteLine("Processing line: " + line);
// After this line finishes, the RAM is cleared for the NEXT line!
}
} // 4. Pipe is automatically closed here.
| Action | Professional Tool | Analogy |
|---|---|---|
| Writing | StreamWriter | The Delivery Truck: It groups your small packages (lines) into one big shipment to the warehouse. |
| Reading | StreamReader | The Conveyor Belt: It brings one item at a time from the warehouse to your desk so you don’t get overwhelmed. |
In professional C#, Stream is the “Parent” (the Base Class). FileStream, MemoryStream, and NetworkStream are the “Children.” Because they are all “Streams,” they all work with the same helpers. C# uses the abstract class Stream to provide a common set of methods (like Read, Write, Seek, and Flush) that all specific stream types must implement.
| Stream Type | Where is the data? | Analogy | Best Used For… |
|---|---|---|---|
| FileStream | On the Disk (HDD/SSD) | The Warehouse: Slow but holds a massive amount of stuff forever. | Saving Games, Logs, and Settings. |
| MemoryStream | In the RAM | The Workbench: Super fast, but everything is cleared when the power goes out. | Moving data internally or testing code without creating real files. |
| NetworkStream | Over the Internet | The Shipping Port: Data comes from far away. It might be slow or get “clogged.” | Multiplayer games, Chat apps, and Web requests. |