Skip to main content

    Lesson 14 • Expert Track

    File I/O

    By the end of this lesson you'll be able to read and write text files in C#, stream large files line by line, manage folders and build cross-platform paths, and parse multi-line data into records — the skills every real app needs to save and load data.

    What You'll Learn

    • Read and write whole files with File.ReadAllText / WriteAllText
    • Append without overwriting, and read line-by-line with ReadAllLines
    • Stream large files efficiently with StreamReader / StreamWriter
    • Free resources automatically with the using statement
    • Build safe, cross-platform paths with Path.Combine and check Exists
    • Parse multi-line / CSV text into records with string Split

    💡 Real-World Analogy

    File.ReadAllText is like photocopying an entire document at once — fast and simple, but it all has to fit on the desk (in memory). A StreamReader is like reading a book one page at a time — slower to get going, but you can work through a 1,000-page novel without it ever overwhelming you. And a using block is the librarian who always returns the book to the shelf when you're done, so the next reader isn't locked out. Pick the photocopier for small files, the page-turner for huge ones.

    A note on running these in the browser: the in-browser runner doesn't have a real, writable disk, so the File.* examples below are written for you to read and learn from (each has its expected output in comments). The interactive 🎯 YOUR TURN exercises use in-memory StringWriter/StringReader and string Split — the exact same logic, minus the disk — so they actually run. To run the real file code, install the .NET SDK locally.

    📊 The File I/O Toolkit

    TypeWhat it doesTypical use
    FileOne-line read/write/append/exists helpersSmall files, quick jobs
    StreamReaderReads a file line by line (or in chunks)Large files, low memory
    StreamWriterWrites lines to a file as a streamLogs, large output, appends
    PathBuilds & inspects paths as stringsCombine, get name/extension
    DirectoryCreates / lists / checks foldersExists, CreateDirectory

    All of these live in the System.IO namespace — forgetting using System.IO; at the top is the most common "the name File doesn't exist" error.

    1. Reading & Writing Whole Files

    The File class gives you one-line methods for the everyday jobs. WriteAllText creates the file (or overwrites it if it exists); AppendAllText adds to the end without erasing anything; ReadAllText loads the whole file into one string; and ReadAllLines gives you a string[] with one element per line. These are perfect for small files — read this worked example, then you'll do the same round-trip in memory.

    Worked example: write, read, append

    Read every comment and the expected output. (Real-file code — run it locally.)

    Try it Yourself »
    C#
    using System;
    using System.IO;          // 👈 File, StreamReader, Path, Directory all live here
    
    class Program
    {
        static void Main()
        {
            // A file path is just a string. "example.txt" is RELATIVE — it lands
            // wherever your program runs from (its working directory).
            string filePath = "example.txt";
    
            // WriteAllText CREATES the file (or OVERWRITES it if it already exists).
            // \n is a newline, so this writes three separate lines.
            File.WriteAllTe
    ...

    Your turn. A StringWriter/StringReader behaves just like a file but lives in memory, so this one runs right here. Fill in the two ___ blanks, then run it.

    🎯 Your turn: an in-memory write/read round-trip

    Write three lines, then read them back. Check the numbered output.

    Try it Yourself »
    C#
    using System;
    using System.IO;
    
    class Program
    {
        static void Main()
        {
            // 🎯 YOUR TURN — same idea as a file, but IN MEMORY so it runs here.
            // A StringWriter behaves like a file you can write lines to;
            // a StringReader behaves like a file you can read lines from.
    
            // 1) Write three lines into the StringWriter.
            var writer = new StringWriter();
            writer.WriteLine("apples");
            writer.WriteLine("bananas");
            writer.___("cherries");   
    ...

    2. StreamReader, StreamWriter & using

    When a file is large, you don't want the whole thing in memory at once. Streams let you process it a line (or chunk) at a time. The catch: a stream holds an open handle on the file, and you must close it — otherwise the file stays locked and your last writes may never be saved. The using statement does this for you: it closes and flushes the stream automatically at the end of the block, even if an exception is thrown. Treat using as mandatory for every stream.

    Worked example: streams with using

    Write five records with StreamWriter, read them back with StreamReader. (Run locally.)

    Try it Yourself »
    C#
    using System;
    using System.IO;
    
    class Program
    {
        static void Main()
        {
            string filePath = "data.txt";
    
            // 'using' opens the stream and GUARANTEES it is closed/flushed at the
            // end of the block — even if an error is thrown. Forgetting this is the
            // #1 file bug: the file stays locked and your data may not be saved.
            using (StreamWriter writer = new StreamWriter(filePath))
            {
                for (int i = 1; i <= 5; i++)
                    writer.WriteLi
    ...

    3. Directories & Paths

    Never glue paths together with + and a slash — Windows uses \\ and Linux/Mac use /, and hard-coding one breaks the other. Path.Combine inserts the correct separator for you. Directory.Exists / CreateDirectory manage folders, and File.Exists lets you check before you read so you don't crash on a missing file. For literal Windows paths, a verbatim string @"..." means you don't have to escape every backslash.

    Worked example: Path.Combine, Exists & friends

    Build a cross-platform path, create a folder, check existence. (Run locally.)

    Try it Yourself »
    C#
    using System;
    using System.IO;
    
    class Program
    {
        static void Main()
        {
            // Build paths with Path.Combine — it inserts the correct separator
            // (\ on Windows, / on Linux/Mac), so your code works everywhere.
            string dir = "reports";
            string file = Path.Combine(dir, "march.txt");   // reports/march.txt (or reports\march.txt)
            Console.WriteLine($"Target: {file}");
    
            // Directory.Exists / CreateDirectory — make the folder only if missing.
            if (!
    ...

    Reading a file usually ends with parsing its text into useful pieces. Here you'll take a multi-line, comma-separated string (exactly what ReadAllText would hand you) and split it into records. Fill in the two ___ blanks, then run it.

    🎯 Your turn: parse multi-line CSV text into records

    Split on the newline to get rows, then on the comma to get fields.

    Try it Yourself »
    C#
    using System;
    
    class Program
    {
        static void Main()
        {
            // 🎯 YOUR TURN — parse multi-line text into records (no real file needed).
            // This is EXACTLY what you'd do after File.ReadAllText, just in memory.
    
            // One string holding three CSV-style rows separated by \n (newline).
            string csv = "Alice,30\nBob,25\nCarol,41";
    
            // 1) Split the whole block into individual lines on the newline '\n'.
            string[] rows = csv.Split('___');     // 👉 split on the 
    ...

    🔎 Deep Dive: File.* vs streams vs in-memory strings

    The File helpers (ReadAllText, WriteAllText) are convenience wrappers: under the hood they open a stream, do the work, and close it for you. That's why they're great for small files — and why you reach for a raw StreamReader/StreamWriter when a file is too big to hold in memory or you want to write thousands of lines without rebuilding one giant string.

    The third option — StringWriter / StringReader — uses the same stream API but the "file" is just an in-memory string. It's perfect for tests, for building text to return from a method, and (handily) for learning file logic in an environment with no disk.

    // Same shape, three backings:
    File.WriteAllText("f.txt", text);              // disk, one shot
    using var sw = new StreamWriter("f.txt");      // disk, streamed
    using var mw = new StringWriter();             // memory, streamed (no disk!)

    For web apps, prefer the async versions — ReadAllTextAsync, WriteAllTextAsync — so a slow disk doesn't block other requests.

    Putting It Together: a Tiny Note-Saver

    This small program uses everything from the lesson at once — it makes a folder, builds a cross-platform path, writes a header, appends entries through a using stream, then reads it all back and counts the entries. You understand every line now. (It writes to disk, so run it locally.)

    Worked example: a note-saver

    Create a folder, append a log with a stream, then read & count entries.

    Try it Yourself »
    C#
    using System;
    using System.IO;
    
    class Program
    {
        static void Main()
        {
            // === A tiny note-saver: write a log, append to it, then read it back ===
            string folder = "app-data";
            if (!Directory.Exists(folder))
                Directory.CreateDirectory(folder);
    
            string logPath = Path.Combine(folder, "activity.log");
    
            // Start fresh each run (overwrite), then append entries with a stream.
            File.WriteAllText(logPath, "=== Activity Log ===\n");
            usi
    ...

    Note new StreamWriter(logPath, append: true) — passing append: true adds to the file instead of overwriting it. Leave it off (the default) and you start fresh each time.

    Pro Tips

    • 💡 Under ~1 MB? Use File.ReadAllText/WriteAllText. They're simplest and the disk isn't the bottleneck at that size.
    • 💡 Over ~1 MB, or unknown size? Stream it with StreamReader/StreamWriter so you never blow up memory.
    • 💡 Always wrap streams in using — it guarantees the file is flushed and unlocked, even if an error is thrown.
    • 💡 Build paths with Path.Combine, not +, so your code runs unchanged on Windows, Linux, and Mac.
    • 💡 Use async in web apps: await File.ReadAllTextAsync(path) keeps the server responsive while the disk works.

    Common Errors (and the fix)

    • "CS0103: The name 'File' does not exist in the current context": you forgot the namespace. Add using System.IO; at the top — File, StreamReader, Path and Directory all live there.
    • "System.IO.FileNotFoundException: Could not find file …": you tried to read a file that isn't there. Guard reads with if (File.Exists(path)), and double-check the path — relative paths are resolved from the program's working directory, not the source folder.
    • "System.IO.DirectoryNotFoundException: Could not find a part of the path …": a folder in the path doesn't exist. Create it first with Directory.CreateDirectory(folder) before writing into it.
    • "System.IO.IOException: The process cannot access the file … because it is being used by another process": a stream is still open (yours or another program's). Wrap your streams in using so they close promptly, and don't open the same file twice at once.
    • Data missing or truncated after writing: you wrote with a StreamWriter but never closed it, so the buffer was never flushed. Use a using block (or call writer.Flush()/Dispose()) to force the write.
    • Path looks wrong on another OS: you hard-coded "folder\\file.txt". Use Path.Combine("folder", "file.txt") so the separator is correct everywhere.

    📋 Quick Reference

    TaskCodeNotes
    Write (overwrite)File.WriteAllText(p, text)Creates or replaces
    AppendFile.AppendAllText(p, text)Keeps existing content
    Read allFile.ReadAllText(p)Whole file → string
    Read linesFile.ReadAllLines(p)→ string[]
    Stream writeusing var w = new StreamWriter(p)Big files / logs
    Stream readusing var r = new StreamReader(p)r.ReadLine() in a loop
    In-memory writenew StringWriter()No disk; .ToString()
    Build a pathPath.Combine("a", "b.txt")Cross-platform separator
    Folder exists?Directory.Exists(d)true / false
    File exists?File.Exists(p)Check before reading

    Frequently Asked Questions

    Q: When should I use File.ReadAllText versus a StreamReader?

    Use ReadAllText/ReadAllLines for small files — it's the simplest code and the size doesn't matter. Switch to a StreamReader when the file is large or you don't know its size, so you process it a line at a time without loading it all into memory.

    Q: What does the using statement actually do?

    It guarantees the stream is closed and flushed when the block ends — even if an exception is thrown. Without it, the file can stay locked and your final writes may never reach disk. Always wrap streams in using.

    Q: Why does my program say it can't find a file that's clearly there?

    A relative path like "data.txt" is resolved from the program's working directory (often the build output folder), not your source folder. Print Directory.GetCurrentDirectory() to see where it's looking, or use an absolute path / Path.Combine from a known root.

    Q: How do I add to a file without wiping what's already in it?

    Use File.AppendAllText, or open a StreamWriter with append: true. Plain File.WriteAllText and new StreamWriter(path) overwrite the whole file.

    Mini-Challenge: Total a CSV Column

    No blanks this time — just a brief and an outline. Take the in-memory "product,price" CSV string, split it into rows and then fields, parse each price, and add them up. This is the exact pattern you'd run on the text from File.ReadAllText — and it runs right here, no disk needed. Check your output against the expected line.

    🎯 Mini-Challenge: sum a CSV price column

    Split, parse, and total the prices. The answer should be 15.

    Try it Yourself »
    C#
    using System;
    using System.Collections.Generic;
    
    class Program
    {
        static void Main()
        {
            // 🎯 MINI-CHALLENGE: Total a column from a CSV string
            // The data below is "product,price" — one row per line.
            string csv = "Pen,2\nNotebook,5\nStapler,8";
            //
            // 1. Split csv on '\n' to get the rows.
            // 2. Loop the rows; for each, Split on ',' to get [name, price].
            // 3. Convert the price with int.Parse(...) and add it to a running total.
            
    ...

    🎉 Lesson Complete

    • File.WriteAllText overwrites, AppendAllText adds, ReadAllText/ReadAllLines read
    • StreamReader/StreamWriter handle large files a line at a time
    • Always wrap streams in using so they're closed, flushed, and unlocked
    • Path.Combine builds cross-platform paths; File.Exists/Directory.Exists guard before access
    • StringWriter/StringReader give you the same API in memory — no disk required
    • ✅ Parsing files is just looping lines and Split-ing them into fields
    • You've finished the Expert Track! Next up: Advanced LINQ — query and transform data in elegant, declarative pipelines

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service