ZetCode

C# StringReader

last modified October 18, 2026

In this article we show how to read strings in C# with StringReader.

Input and output in C# is based on streams. A Stream is an abstract base class of all streams. 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. Text is read through classes derived from TextReader.

C# StringReader

StringReader is a TextReader that reads characters from a string. It can read synchronously or asynchronously. It has no underlying resources to release, so disposing it is optional, but we do it by habit.

Why read from a string with a reader at all? Many APIs accept a TextReader: XML and JSON parsers, CSV libraries, template engines, and code that processes input line by line. A StringReader lets us feed such code from memory, which is also handy in unit tests.

C# StringReader ReadToEnd

The ReadToEnd method reads all characters from the current position to the end of the string and returns them as a single string.

Program.cs
using System.Text;

var sb = new StringBuilder();
sb.AppendLine("There is a hawk in the sky.");
sb.AppendLine("The sun is shining.");
sb.AppendLine("The flowers are blossoming.");

using var reader = new StringReader(sb.ToString());
string text = reader.ReadToEnd();
Console.WriteLine(text);

The example builds a string with StringBuilder and then reads the text with the ReadToEnd method of the reader.

$ dotnet run
There is a hawk in the sky.
The sun is shining.
The flowers are blossoming.

The last blank line appears because AppendLine terminates every line, and Console.WriteLine adds one more terminator. Since the string is already in memory, this example is only illustrative; the result is equal to sb.ToString().

C# StringReader ReadLine

The ReadLine method reads a line of characters from the string and returns it without the line terminator.

Program.cs
var text = """
    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.
    """;

using var sr = new StringReader(text);

int count = 0;
string? line;

while ((line = sr.ReadLine()) is not null)
{
    count++;
    Console.WriteLine($"Line {count}: {line}");
}

In the example, we number the lines of a multiline string.

var text = """
    The Battle of Thermopylae was fought between an alliance
    ...
    """;

A raw string literal spans multiple lines. The whitespace to the left of the closing quotes is removed from every line, so the indentation of the source code does not become part of the text.

while ((line = sr.ReadLine()) is not null)
{

The ReadLine method returns null when the end of the string is reached. The variable is declared as string? to reflect that. The recognized terminators are \n, \r, and \r\n.

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

C# StringReader Read

The Read method reads the next character from the input string and advances the position by one character. It returns an int, because it must be able to return -1 at the end of the string.

Program.cs
var text = "There is an old hawk in the sky.";

using var reader = new StringReader(text);

int count = 0;
char myChar = 'h';
int n;

while ((n = reader.Read()) != -1)
{
    if ((char)n == myChar)
    {
        count++;
    }
}

Console.WriteLine($"There are {count} '{myChar}' characters in the string");

In the example, we count the occurrences of the 'h' character in the text.

while ((n = reader.Read()) != -1)
{
    if ((char)n == myChar)
    {
        count++;
    }
}

We read until Read returns -1. Otherwise, the int is cast to a char and compared.

$ dotnet run
There are 3 'h' characters in the string

C# StringReader Peek and ReadBlock

The Peek method returns the next character without consuming it, or -1 at the end. The ReadBlock method reads up to a given number of characters into a buffer and returns the number actually read.

Program.cs
var text = "a=1;b=22;c=333;";

using var reader = new StringReader(text);

var sb = new System.Text.StringBuilder();

while (reader.Peek() != -1)
{
    char c = (char)reader.Read();

    if (c == ';')
    {
        Console.WriteLine($"token: {sb}");
        sb.Clear();
    }
    else
    {
        sb.Append(c);
    }
}

using var reader2 = new StringReader("0123456789");
char[] buf = new char[4];
int n;

while ((n = reader2.ReadBlock(buf, 0, buf.Length)) > 0)
{
    Console.WriteLine($"block of {n}: {new string(buf, 0, n)}");
}

The first loop splits a string into tokens ended by a semicolon. The second reads a string in blocks of four characters.

while (reader.Peek() != -1)

Peek lets us test whether more input is available without advancing the position.

while ((n = reader2.ReadBlock(buf, 0, buf.Length)) > 0)
{
    Console.WriteLine($"block of {n}: {new string(buf, 0, n)}");
}

ReadBlock returns 0 when nothing is left. The last block can be shorter than the buffer, so we use the returned count when building the string.

$ dotnet run
token: a=1
token: b=22
token: c=333
block of 4: 0123
block of 4: 4567
block of 2: 89

C# StringReader parse CSV text

A common task is to process text line by line. In this example, we parse a small CSV document held in a string.

Program.cs
var csv = """
    name,price
    Audi,52642
    Mercedes,57127
    Skoda,9000
    """;

using var sr = new StringReader(csv);

_ = sr.ReadLine(); // skip the header

var cars = new List<(string Name, int Price)>();
string? line;

while ((line = sr.ReadLine()) is not null)
{
    string[] parts = line.Split(',');
    cars.Add((parts[0], int.Parse(parts[1])));
}

foreach (var (name, price) in cars)
{
    Console.WriteLine($"{name,-10} {price,8:N0}");
}

Console.WriteLine($"Total: {cars.Sum(c => c.Price):N0}");

We skip the header line, split each remaining line on commas, and store the values in a list of tuples.

_ = sr.ReadLine(); // skip the header

The discard _ makes it clear that we ignore the returned header.

Console.WriteLine($"{name,-10} {price,8:N0}");

The alignment component and the N0 format specifier produce neat columns with thousands separators. For real-world CSV with quoted fields, use a dedicated parser, as a simple Split breaks on embedded commas.

$ dotnet run
Audi         52,642
Mercedes     57,127
Skoda         9,000
Total: 118,769

C# StringReader async

The reader has asynchronous counterparts: ReadLineAsync, ReadToEndAsync, ReadAsync, and ReadBlockAsync. For a StringReader they complete synchronously, because the data is already in memory, but they keep the code uniform with other TextReader types.

Program.cs
var text = "first line\nsecond line\nthird line";

using var reader = new StringReader(text);

string? line;
int n = 0;

while ((line = await reader.ReadLineAsync()) is not null)
{
    Console.WriteLine($"{++n}: {line}");
}

We read the lines of a string asynchronously.

$ dotnet run
1: first line
2: second line
3: third line

C# StringReader as TextReader

Because StringReader is a TextReader, we can pass it to any API that expects one. We also use Console.SetIn to replace standard input, which is useful for testing console programs.

Program.cs
using System.Xml;

var xml = "<words><word>falcon</word><word>owl</word><word>stone</word></words>";

using var sr = new StringReader(xml);
using var xr = XmlReader.Create(sr);

while (xr.Read())
{
    if (xr.NodeType == XmlNodeType.Text)
    {
        Console.WriteLine(xr.Value);
    }
}

// Console.SetIn replaces standard input with a StringReader, handy for testing
Console.SetIn(new StringReader("42\nhello\n"));
int num = int.Parse(Console.ReadLine()!);
string word = Console.ReadLine()!;
Console.WriteLine($"{num} {word}");

We parse an XML document from a string with XmlReader, and then feed two lines to Console.ReadLine without touching the keyboard.

$ dotnet run
falcon
owl
stone
42 hello

StringReader and line endings

Splitting a string on '\n' leaves stray '\r' characters behind when the text has Windows line endings. StringReader.ReadLine handles all the usual terminators.

Program.cs
var text = "one\ntwo\r\nthree\rfour";

foreach (var line in text.Split('\n'))
{
    Console.WriteLine($"[{line.Replace("\r", "\\r")}]");
}

Console.WriteLine("--");

using var sr = new StringReader(text);
string? l;
while ((l = sr.ReadLine()) is not null)
{
    Console.WriteLine($"[{l}]");
}

Console.WriteLine("--");

foreach (var line in text.AsSpan().EnumerateLines())
{
    Console.WriteLine($"[{line}]");
}

The text mixes \n, \r\n, and a lone \r.

$ dotnet run
[one]
[two\r]
[three\rfour]
--
[one]
[two]
[three]
[four]
--
[one]
[two]
[three]
[four]

The first block shows the problem with Split('\n'). The second uses StringReader. The third uses EnumerateLines, which works on a ReadOnlySpan<char> and does not allocate a string per line, so it is the better choice when we only need to iterate over lines of a string already in memory. The string.ReplaceLineEndings method can also normalize the endings.

Source

StringReader class

In this article we have read strings in C# with StringReader.

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.