ZetCode

C# input & output

last modified October 16, 2026

In this article we cover input & output operations in C#. The input & output in C# is based on streams.

C# stream

A stream is an abstraction of a sequence of bytes, such as a file, an input/output device, an inter-process communication pipe, or a TCP/IP socket. Streams transfer data from one point to another point. Streams are also capable of manipulating the data; for example, they can compress or encrypt the data. In .NET, the System.IO namespace contains types that enable reading and writing on data streams and files.

C# provides higher-level methods for I/O operations in the File class and lower-level methods in classes such as StreamReader, StreamWriter, or FileStream.

Handling exceptions

I/O operations are error-prone. We can run into exceptions such as FileNotFoundException or UnauthorizedAccessException. Unlike Java, C# does not force the programmer to handle exceptions. If an exception is not handled in a try/catch statement, the runtime terminates the program and prints the error.

Releasing resources

I/O resources, such as open files, must be released. The resources can be released manually in the finally clause with the Dispose method. The using statement and the using declaration release the resources automatically. The methods of the File class release the resources for us.

Example text file

In the examples, we use this simple text file. It is located in the project directory, from which we run the programs with dotnet run.

thermopylae.txt
The Battle of Thermopylae was fought between an alliance of Greek city-states,
led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the
course of three days, during the second Persian invasion of Greece.

C# File.ReadAllText

File provides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of FileStream objects.

The File.ReadAllText method opens a file, reads all text in the file with the specified encoding, and then closes the file.

Program.cs
using System.Text;

var path = "thermopylae.txt";

var text = File.ReadAllText(path, Encoding.UTF8);
Console.WriteLine(text);

The program reads the contents of the thermopylae.txt file and prints them to the console.

var text = File.ReadAllText(path, Encoding.UTF8);

We read the whole file into a string in one shot. In the second argument, we specify the encoding. UTF-8 is the default, so the argument can be omitted.

$ dotnet run
The Battle of Thermopylae was fought between an alliance of Greek city-states,
led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the
course of three days, during the second Persian invasion of Greece.
Note: File.ReadAllText and File.ReadAllLines load the whole file into memory. They are not suited for reading very large files.

C# File.ReadAllLines

File.ReadAllLines opens a text file, reads all lines of the file into a string array, and then closes the file.

Program.cs
var path = "thermopylae.txt";

string[] lines = File.ReadAllLines(path);

Console.WriteLine($"The file has {lines.Length} lines");
Console.WriteLine(lines[0]);

The example reads all lines from a file into a string array. We print the number of lines and the first line.

$ dotnet run
The file has 3 lines
The Battle of Thermopylae was fought between an alliance of Greek city-states,

C# File.ReadLines

The File.ReadLines method reads the lines lazily, one by one, as we enumerate them. Only the current line is kept in memory; therefore, it is suitable for large files.

Program.cs
var path = "thermopylae.txt";

int n = 1;

foreach (var line in File.ReadLines(path))
{
    Console.WriteLine($"{n++}: {line}");
}

We print the lines of the file with line numbers.

$ dotnet run
1: The Battle of Thermopylae was fought between an alliance of Greek city-states,
2: led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the
3: course of three days, during the second Persian invasion of Greece.

C# write file

The File.WriteAllText and File.WriteAllLines methods create a file, write the data, and close the file. If the file already exists, it is overwritten. The File.AppendAllText and File.AppendAllLines methods add data to the end of a file.

Program.cs
var path = "words.txt";

string[] words = ["sky", "cloud", "forest"];

File.WriteAllLines(path, words);
File.AppendAllText(path, "ocean\n");

Console.WriteLine(File.ReadAllText(path));

We write three words to a file, append a fourth one, and print the contents of the file.

$ dotnet run
sky
cloud
forest
ocean

C# create file

The File.CreateText method creates or opens a file for writing UTF-8 encoded text. If the file already exists, its contents are overwritten.

Program.cs
var path = "cars.txt";

using (var sw = File.CreateText(path))
{
    sw.WriteLine("Hummer");
    sw.WriteLine("Skoda");
    sw.WriteLine("BMW");
    sw.WriteLine("Volkswagen");
    sw.WriteLine("Volvo");
}

Console.WriteLine(File.ReadAllText(path));

In the example, we create a cars.txt file and write some car names into it.

using (var sw = File.CreateText(path))

The CreateText method returns a StreamWriter object. The data is written to the file when the writer is disposed at the end of the using block; only then can we read the complete file.

sw.WriteLine("Hummer");
sw.WriteLine("Skoda");
...

We write five lines to the stream.

$ dotnet run
Hummer
Skoda
BMW
Volkswagen
Volvo

C# file times

With the File class, we can get the creation, last write and last access times of a file. The Exists method determines whether the specified file exists.

Program.cs
var path = "cars.txt";

if (File.Exists(path))
{
    Console.WriteLine(File.GetCreationTime(path));
    Console.WriteLine(File.GetLastWriteTime(path));
    Console.WriteLine(File.GetLastAccessTime(path));
}

If the file exists, we print its creation, last write, and last access times.

if (File.Exists(path))

The Exists method returns true if the caller has the required permissions and the path contains the name of an existing file; otherwise, false. This method also returns false if the path is null, an invalid path, or a zero-length string.

$ dotnet run
10/16/2026 7:13:38 PM
10/16/2026 7:13:38 PM
10/16/2026 7:13:38 PM

C# copy file

The File.Copy method copies an existing file to a new file.

Program.cs
var srcPath = "cars.txt";
var destPath = "cars2.txt";

File.Copy(srcPath, destPath, true);

Console.WriteLine($"File copied: {new FileInfo(destPath).Length} bytes");

We copy the cars.txt file to cars2.txt.

File.Copy(srcPath, destPath, true);

The third parameter specifies whether the destination file should be overwritten if it exists. Without it, an IOException is thrown.

$ dotnet run
File copied: 34 bytes

C# IDisposable interface

Streams implement the IDisposable interface. Objects that implement this interface should be disposed at the earliest opportunity. This is done by calling the Dispose method in a finally block or by utilizing the using statement.

Program.cs
StreamReader? sr = null;
var path = "thermopylae.txt";

try
{
    sr = new StreamReader(path);
    Console.WriteLine(sr.ReadToEnd());
}
catch (IOException e)
{
    Console.WriteLine("Cannot read file");
    Console.WriteLine(e.Message);
}
catch (UnauthorizedAccessException e)
{
    Console.WriteLine("Cannot access file");
    Console.WriteLine(e.Message);
}
finally
{
    sr?.Dispose();
}

In this example, we read characters from a file on a disk. We manually release allocated resources.

sr = new StreamReader(path);
Console.WriteLine(sr.ReadToEnd());

The StreamReader class is used to read characters. It implements the IDisposable interface.

catch (IOException e)
{
    Console.WriteLine("Cannot read file");
    Console.WriteLine(e.Message);
}
catch (UnauthorizedAccessException e)
{
    Console.WriteLine("Cannot access file");
    Console.WriteLine(e.Message);
}

Possible exceptions are handled in the catch blocks.

finally
{
    sr?.Dispose();
}

In the finally block, the Dispose method cleans up the resources. With the null-conditional operator, we call the method only if the variable is not null; it is null when the file could not be opened.

$ dotnet run
The Battle of Thermopylae was fought between an alliance of Greek city-states,
led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the
course of three days, during the second Persian invasion of Greece.

C# using statement

The using statement defines a scope at the end of which an object is disposed. It provides a convenient syntax that ensures the correct use of IDisposable objects.

Program.cs
var path = "thermopylae.txt";

using (var sr = new StreamReader(path))
{
    Console.WriteLine(sr.ReadToEnd());
}

The example reads the contents of the thermopylae.txt file. The resources are released at the end of the using block, even if an exception is thrown inside it.

$ dotnet run
The Battle of Thermopylae was fought between an alliance of Greek city-states,
led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the
course of three days, during the second Persian invasion of Greece.

C# using declaration

The using declaration is a variable declaration preceded by the using keyword. It tells the compiler that the variable should be disposed at the end of the enclosing scope. The using declaration is available since C# 8.0.

Program.cs
var path = "thermopylae.txt";

using var sr = new StreamReader(path);
Console.WriteLine(sr.ReadToEnd());

The reader is disposed when the sr variable goes out of scope; in our program, at the end of the top-level statements.

$ dotnet run
The Battle of Thermopylae was fought between an alliance of Greek city-states,
led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the
course of three days, during the second Persian invasion of Greece.

C# MemoryStream

MemoryStream is a stream which works with data in computer memory.

Program.cs
using var ms = new MemoryStream(6);

ms.WriteByte(9);
ms.WriteByte(11);
ms.WriteByte(6);
ms.WriteByte(8);
ms.WriteByte(3);
ms.WriteByte(7);

ms.Position = 0;

int rs = ms.ReadByte();

while (rs != -1)
{
    Console.WriteLine(rs);
    rs = ms.ReadByte();
}

We write six numbers to memory with a MemoryStream. Then we read those numbers and print them to the console.

using var ms = new MemoryStream(6);

The line creates a MemoryStream object with an initial capacity of six bytes. The capacity grows when needed.

ms.WriteByte(9);
ms.WriteByte(11);
ms.WriteByte(6);
...

The WriteByte method writes a byte to the stream at the current position.

ms.Position = 0;

We set the position in the stream back to the beginning using the Position property.

while (rs != -1)
{
    Console.WriteLine(rs);
    rs = ms.ReadByte();
}

Here we read all bytes from the stream and print them to the console. The ReadByte method returns -1 at the end of the stream.

$ dotnet run
9
11
6
8
3
7

C# StreamReader

StreamReader reads characters from a byte stream. It defaults to UTF-8 encoding.

Program.cs
var path = "thermopylae.txt";

using var sr = new StreamReader(path);

string? line;

while ((line = sr.ReadLine()) != null)
{
    Console.WriteLine(line.ToUpper());
}

We read the contents of a file line by line using the ReadLine method and print each line in uppercase.

while ((line = sr.ReadLine()) != null)

The ReadLine method returns the next line of the stream, or null if the end of the stream is reached.

$ dotnet run
THE BATTLE OF THERMOPYLAE WAS FOUGHT BETWEEN AN ALLIANCE OF GREEK CITY-STATES,
LED BY KING LEONIDAS OF SPARTA, AND THE PERSIAN EMPIRE OF XERXES I OVER THE
COURSE OF THREE DAYS, DURING THE SECOND PERSIAN INVASION OF GREECE.

C# count lines

In the next example, we count the lines of a file.

Program.cs
int count = 0;
var path = "thermopylae.txt";

using var sr = new StreamReader(path);

while (sr.ReadLine() != null)
{
    count++;
}

Console.WriteLine($"There are {count} lines");

We use a StreamReader and a while loop. The same result can be achieved with File.ReadLines(path).Count().

$ dotnet run
There are 3 lines

C# StreamWriter

StreamWriter writes characters to a stream in a particular encoding.

Program.cs
var path = "newfile.txt";

using (var sw = new StreamWriter(path))
{
    sw.WriteLine("Today is a beautiful day.");
}

using (var sw = new StreamWriter(path, append: true))
{
    sw.WriteLine("We go swimming.");
}

Console.Write(File.ReadAllText(path));

The example writes a line to a new file with StreamWriter and then appends another line.

using (var sw = new StreamWriter(path))

We create a new StreamWriter. The default encoding is UTF-8. If the file exists, it is overwritten; otherwise, a new file is created.

using (var sw = new StreamWriter(path, append: true))

With the append parameter set to true, the data is added to the end of an existing file.

$ dotnet run
Today is a beautiful day.
We go swimming.

C# FileStream

FileStream provides a stream for a file, supporting both synchronous and asynchronous read and write operations. StreamReader and StreamWriter work with text data, while FileStream works with bytes.

Program.cs
using System.Text;

var path = "newfile2.txt";

using (var fs = new FileStream(path, FileMode.Create))
{
    var text = "Фёдор Михайлович Достоевский\n";
    byte[] bytes = Encoding.UTF8.GetBytes(text);

    fs.Write(bytes, 0, bytes.Length);
    Console.WriteLine($"{text.Length} characters, {bytes.Length} bytes");
}

Console.Write(File.ReadAllText(path));

We write some text in Russian Cyrillic to a file.

using (var fs = new FileStream(path, FileMode.Create))

A FileStream object is created. The second parameter is the mode in which the file is opened. The Create mode creates a new file or overwrites an existing one. With FileMode.Append, we would add the data to the end of the file.

byte[] bytes = Encoding.UTF8.GetBytes(text);

The text is encoded into an array of bytes. In UTF-8, each Cyrillic letter takes two bytes.

fs.Write(bytes, 0, bytes.Length);

We write the bytes to the file stream.

$ dotnet run
29 characters, 55 bytes
Фёдор Михайлович Достоевский

C# asynchronous file I/O

The File class has asynchronous versions of its methods. They do not block the calling thread while waiting for the disk, which matters in GUI and server applications.

Program.cs
var path = "thermopylae.txt";

string text = await File.ReadAllTextAsync(path);
Console.WriteLine($"{text.Length} characters");

await foreach (var line in File.ReadLinesAsync(path))
{
    Console.WriteLine(line.Split(' ')[0]);
}

await File.WriteAllTextAsync("copy.txt", text);
Console.WriteLine(File.Exists("copy.txt"));

We read the file asynchronously as a whole and line by line, and write a copy of it.

await foreach (var line in File.ReadLinesAsync(path))

The File.ReadLinesAsync method, available since .NET 7, returns an IAsyncEnumerable<string>, which we iterate with await foreach. We print the first word of each line.

$ dotnet run
223 characters
The
led
course
True

C# console input

The console is a stream too. The Console.ReadLine method reads a line from the standard input. It returns null when the input ends, for example when the input is redirected from a file or a pipe.

Program.cs
int lines = 0;
int words = 0;

string? line;

while ((line = Console.ReadLine()) != null)
{
    lines++;
    words += line.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}

Console.WriteLine($"lines: {lines}, words: {words}");

The program counts the lines and words of its standard input.

$ cat thermopylae.txt | dotnet run
lines: 3, words: 38

We pipe the contents of the file to the program. When we run the program interactively, we type the text and end the input with Ctrl+D on Unix or Ctrl+Z and Enter on Windows.

C# XmlReader

We can use streams to read XML data. The XmlReader class provides fast, non-cached, forward-only access to XML data.

We have the following XML test file:

languages.xml
<?xml version="1.0" encoding="utf-8" ?>
<languages>
    <language>Python</language>
    <language>Ruby</language>
    <language>JavaScript</language>
    <language>C#</language>
</languages>

This file contains language names between custom XML tags.

Program.cs
using System.Xml;

var path = "languages.xml";

using var reader = XmlReader.Create(path);

while (reader.Read())
{
    if (reader.NodeType == XmlNodeType.Element && reader.Name == "language")
    {
        Console.WriteLine($"{reader.Name}: {reader.ReadElementContentAsString()}");
    }
}

This example reads data from the XML file and prints it to the terminal.

using var reader = XmlReader.Create(path);

An XmlReader object is created with the Create factory method. The older XmlTextReader class still works, but XmlReader.Create is the recommended way.

while (reader.Read())

The Read method moves to the next node of the document. It returns false if there are no more nodes left.

Console.WriteLine($"{reader.Name}: {reader.ReadElementContentAsString()}");

For each language element, we print its name and its text content.

$ dotnet run
language: Python
language: Ruby
language: JavaScript
language: C#

C# create and move directory

The Directory class has static methods for creating, moving, and enumerating through directories and subdirectories.

Program.cs
Directory.CreateDirectory("temp");
Directory.CreateDirectory("newdir");

if (!Directory.Exists("temporary"))
{
    Directory.Move("temp", "temporary");
}

Console.WriteLine(Directory.Exists("temp"));
Console.WriteLine(Directory.Exists("temporary"));
Console.WriteLine(Directory.Exists("newdir"));

We create two directories and rename one of them. The directories are created in the current working directory.

Directory.CreateDirectory("temp");

The CreateDirectory method creates a new directory. It does nothing if the directory already exists.

Directory.Move("temp", "temporary");

The Move method gives a directory a new name. It throws an IOException if the destination already exists; therefore, we check it first.

$ dotnet run
False
True
True

C# DirectoryInfo

DirectoryInfo exposes instance methods for creating, moving, and enumerating through directories and subdirectories.

Program.cs
var dirInfo = new DirectoryInfo(".");

Console.WriteLine("Directories:");

foreach (var dir in dirInfo.EnumerateDirectories().OrderBy(d => d.Name))
{
    Console.WriteLine($"  {dir.Name}");
}

Console.WriteLine("Text files:");

foreach (var file in dirInfo.EnumerateFiles("*.txt").OrderBy(f => f.Name))
{
    Console.WriteLine($"  {file.Name,-16} {file.Length,5} B");
}

We use the DirectoryInfo class to list the subdirectories and the text files of the current directory.

foreach (var dir in dirInfo.EnumerateDirectories().OrderBy(d => d.Name))

The EnumerateDirectories method returns the subdirectories as DirectoryInfo objects. The order is not guaranteed, so we sort them by name.

foreach (var file in dirInfo.EnumerateFiles("*.txt").OrderBy(f => f.Name))

The EnumerateFiles method takes a search pattern. It returns FileInfo objects, which give us the name and the size of each file.

$ dotnet run
Directories:
  bin
  newdir
  obj
  temporary
Text files:
  cars.txt            34 B
  cars2.txt           34 B
  copy.txt           223 B
  newfile.txt         42 B
  newfile2.txt        55 B
  thermopylae.txt    223 B
  words.txt           23 B

The directories and files were created by the previous examples; the bin and obj directories belong to the project.

Source

File and Stream I/O

In this article we have covered input/output operations in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.