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.
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.
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.
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.
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.
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.
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.
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.
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
In this article we have read strings in C# with StringReader.
Author
List all C# tutorials.