I/O Operations in C#
Learn console output and input, formatted printing, reading and writing files, and I/O error handling in C# with the .NET Base Class Library, all runnable with Docker
Input and output are how a program talks to the outside world — printing results to the terminal, reading what a user types, and saving data to files so it survives after the program exits. You already met Console.WriteLine in Hello World; this tutorial goes deeper into formatting, reading input, and moving data to and from disk.
C# handles I/O through the .NET Base Class Library (BCL), a rich standard library that ships with the runtime. The System.Console class covers the terminal, while the System.IO namespace provides File, StreamReader, and StreamWriter for files. As a multi-paradigm language, C# gives you both high-level convenience methods (File.ReadAllText reads an entire file in one call) and lower-level stream objects (StreamReader reads a huge file one line at a time without loading it all into memory).
Because C# is statically and strongly typed, input that arrives as text must be parsed into the type you want, and the compiler makes you handle the possibility that a value is missing (null) or that a file operation throws. This tutorial covers formatted console output, reading typed input from the keyboard, writing and appending files, reading files back, and handling the errors that I/O inevitably produces — each as a complete, runnable program.
Console Output and Formatting
Beyond simple printing, C# offers string interpolation ($"..."), composite formatting with numbered placeholders, and format specifiers that control decimals and alignment. There are also two output streams: Console.Out for normal results (stdout) and Console.Error for diagnostics (stderr), so errors can be redirected independently of your program’s actual output.
Create a file named Program.cs:
| |
String interpolation reads left to right and is the idiomatic choice for most output. The :F2 after a value is a format specifier — F2 means fixed-point with two digits after the decimal — and ,8 reserves a field eight characters wide so columns line up.
Run it:
| |
Expected output (the stderr line appears interleaved in your terminal):
No newline here... but this ends the line.
Diagnostics go to stderr
Count: 42, Price: 19.99, Active: True
Composite: 42 items at 19.99 each
Two decimals: 19.99
Three decimals: 3.142
|Left | Right|
Reading Console Input
Console.ReadLine() reads one line from standard input and returns it as a string? — the ? means it can be null when input ends. Because everything arrives as text, you parse numeric input with int.Parse (which throws on bad input) or the safer int.TryParse, which returns false instead of throwing and hands back the parsed value through an out parameter.
Create a file named Program.cs:
| |
int.Parse throws a FormatException if the text isn’t a number, while int.TryParse returns a bool telling you whether it succeeded — prefer TryParse for anything a user types. The ! after ageText is the null-forgiving operator, telling the compiler you’re confident the value isn’t null here.
This example needs input on stdin, so pipe values in with printf and add the -i flag to docker run:
| |
Expected output (the prompts share a line because Console.Write adds no newline and piped input isn’t echoed):
Enter your name: Enter your age: Enter a favorite number: Hello, Alice!
Next year you will be 31.
Your favorite number squared is 49.
Writing to Files
The File class in System.IO provides one-call helpers for the common cases: File.WriteAllText creates or overwrites a file, File.AppendAllText adds to the end, and File.AppendAllLines writes each element of a collection as its own line. For incremental writing, a StreamWriter inside a using block writes piece by piece and automatically flushes and closes the file when the block ends.
Create a file named Program.cs:
| |
Use File.WriteAllText when you have all the content up front, and a StreamWriter when you build output gradually or want to avoid holding a large string in memory. The using statement is the key detail: it guarantees the file is closed even if an error occurs, so you never leak an open file handle.
Run it with the same Docker command shown above. Expected output:
Finished writing to output.txt
The file now has 7 lines.
Reading Files
Reading mirrors writing. File.ReadAllText slurps the whole file into one string, File.ReadAllLines returns an array of lines, and a StreamReader reads one line at a time — the memory-friendly choice for large files. The while ((line = reader.ReadLine()) != null) loop is the idiomatic streaming pattern, stopping when ReadLine returns null at end of file.
Create a file named Program.cs:
| |
string.Split(',') turns each comma-separated line into an array of fields — a tiny taste of parsing structured text. Choose ReadAllLines for small files where an array is convenient, and a StreamReader loop for files too large to comfortably hold in memory all at once.
Run it with the same Docker command. Expected output:
--- Whole file ---
Alice,30
Bob,25
Carol,41
--- 3 lines ---
Alice is 30 years old
Bob is 25 years old
Carol is 41 years old
--- Streamed ---
1: Alice,30
2: Bob,25
3: Carol,41
Handling I/O Errors
I/O fails for reasons outside your control: a file is missing, a disk is full, or you lack permission. C# reports these failures as exceptions, and idiomatic code wraps risky operations in try/catch. The System.IO exceptions form a hierarchy — FileNotFoundException derives from IOException — so you can catch the specific case first and fall back to the general one. A finally block runs whether or not an error occurred, making it the right place for cleanup.
Create a file named Program.cs:
| |
Catch the most specific exception you can handle meaningfully — here FileNotFoundException gets a friendly message while other IOExceptions report their raw message. The finally block deletes the temporary file no matter what happens in the try, which is exactly how you avoid leaving stray files behind after an error.
Run it with the same Docker command. Expected output:
Could not read 'does_not_exist.txt' - it does not exist.
'does_not_exist.txt' does not exist - skipping read.
Read back: temporary content
Temporary file cleaned up.
Running with Docker
Each example above is a complete program. Save it as Program.cs and run it with the official .NET SDK image — no local install required. The command preserves your Program.cs, scaffolds a temporary project around it, compiles, and runs it.
| |
The examples that create files (output.txt, sample.txt) write them into your mounted directory, so you can inspect them afterward. The System and System.IO namespaces are imported automatically by the SDK’s implicit usings, which is why Console, File, and StreamReader work without any using directives.
Key Concepts
- The BCL handles I/O —
System.Consolecovers the terminal andSystem.IOcovers files; both ship with the runtime and need no external packages. - Formatting is expressive — string interpolation (
$"{value:F2}"), composite formatting ({0}), format specifiers (F2,F3), and alignment ({value,8}) shape how output looks. - stdout vs stderr —
Console.WriteLinewrites results to stdout whileConsole.Error.WriteLinewrites diagnostics to stderr, so the two can be redirected separately. - Input is text; parse it —
Console.ReadLinereturns astring?, and you convert to numbers withint.Parse(throws on bad input) or the saferint.TryParse(returns abool). - Convenience vs streaming —
File.ReadAllText/WriteAllTexthandle a whole file in one call, whileStreamReader/StreamWriterprocess it incrementally to keep memory use low on large files. usingreleases resources — wrapping a stream in ausingblock flushes and closes the file automatically, even if an exception is thrown mid-operation.- I/O errors are exceptions — wrap risky calls in
try/catch, catch specific types likeFileNotFoundExceptionbefore the generalIOException, and usefinallyfor cleanup that must always run.
Running Today
All examples can be run using Docker:
docker pull mcr.microsoft.com/dotnet/sdk:9.0
Comments
Loading comments...
Leave a Comment