Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 19 – Files, JSON, XML, dates, databases and HTTP

Streams, compression and System.IO.Pipelines

Stack streams in C#, compress with GZip and Brotli, catch truncated and oversized archives, and parse large input in flat memory with System.IO.Pipelines.

  • Advanced
  • 20 minutes
  • Examples run with .NET 10.0.401
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: Streams hand bytes to one another, and disposing finishes the job · GZip is the format every gzip tool reads, Brotli makes text the smallest · System.IO.Pipelines lends you buffers instead of filling yours. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Write text through a StreamWriter, a GZipStream and a FileStream, and dispose them so that the output is complete
  • Choose GZip, Brotli or a ZIP archive, and a compression level, from measured sizes
  • Detect truncated and oversized compressed input, and extract a ZIP archive without writing outside its folder
  • Parse a large stream in flat memory with PipeReader, SequenceReader and AdvanceTo

Before you start

A helper that gzips an export into memory returned 10 bytes for 132,227 bytes of CSV, and the 10 bytes decompressed to nothing, with no exception anywhere. One using var in the wrong place caused it: a compressing stream: A sequence of bytes read or written in pieces through the Stream class. FileStream and MemoryStream hold the bytes; wrapping streams such as GZipStream change them on their way to the stream they wrap. writes its last bytes only when it is disposed, and the helper read the result before that.

You will stack streams for text and compression, choose between GZip and Brotli from measured sizes, and meet System.IO.Pipelines, which parses input in buffers it lends you instead of in strings you allocate.

Streams hand bytes to one another, and disposing finishes the job

stack.cs writes 10,000 fares as CSV through three streams, with a stream that counts bytes between each pair, then compresses the same CSV into memory twice:

stack.cs: three streams stacked, and a GZipStream disposed too late C# · stack/stack.cs
// Three streams stacked: a StreamWriter turns text into UTF-8 bytes, a GZipStream compresses them and a FileStream
// writes the result to a file. A counting stream between each pair shows how many bytes pass. Then the same CSV is
// compressed into memory twice: once with the GZipStream disposed too late, once in time.
using System.IO.Compression;
using System.Text;

Directory.CreateDirectory("out");
string rows = MakeCsv(10_000);
Console.WriteLine($"CSV text: {rows.Length:N0} characters");

var toFile = new CountingStream(File.Create(Path.Join("out", "fares.csv.gz")));
var toGzip = new CountingStream(new GZipStream(toFile, CompressionLevel.Optimal));
using (var writer = new StreamWriter(toGzip, new UTF8Encoding(false)))
    writer.Write(rows);
Console.WriteLine($"into the GZipStream: {toGzip.Written:N0} bytes; into the FileStream: {toFile.Written:N0} bytes");
Console.WriteLine($"read back through FileStream, GZipStream and StreamReader: {CountLines(File.ReadAllBytes(Path.Join("out", "fares.csv.gz")))} lines");

byte[] data = Encoding.UTF8.GetBytes(rows);
byte[] late = CompressTooLate(data);
byte[] onTime = Compress(data);
Console.WriteLine($"disposed too late: {late.Length:N0} bytes, which decompress to {CountLines(late)} lines, and nothing threw");
Console.WriteLine($"disposed in time:  {onTime.Length:N0} bytes, which decompress to {CountLines(onTime)} lines");
Console.WriteLine($"can seek? FileStream {File.OpenRead(Path.Join("out", "fares.csv.gz")).CanSeek}, GZipStream {new GZipStream(Stream.Null, CompressionMode.Decompress).CanSeek}");

// The bug: a using declaration disposes at the end of the method, after ToArray has copied the buffer.
static byte[] CompressTooLate(byte[] data)
{
    var output = new MemoryStream();
    using var gzip = new GZipStream(output, CompressionLevel.Optimal);
    gzip.Write(data);
    return output.ToArray();
}

// The fix: the using statement ends, and the GZipStream writes its last block and its footer, before ToArray.
static byte[] Compress(byte[] data)
{
    var output = new MemoryStream();
    using (var gzip = new GZipStream(output, CompressionLevel.Optimal, leaveOpen: true))
        gzip.Write(data);
    return output.ToArray();
}

static int CountLines(byte[] gzip)
{
    using var reader = new StreamReader(new GZipStream(new MemoryStream(gzip), CompressionMode.Decompress));
    int count = 0;
    while (reader.ReadLine() is not null) count++;
    return count;
}

static string MakeCsv(int count)
{
    var text = new StringBuilder("from,to,fare\n");
    string[] stations = ["PUNE", "NDLS", "MAS", "HWH", "SBC", "ADI"];
    for (int i = 0; i < count; i++)
        text.Append(stations[i % 6]).Append(',').Append(stations[(i * 5 + 1) % 6]).Append(',').Append(200 + (i * 37) % 1800).Append('\n');
    return text.ToString();
}

// A stream that passes every write on to the stream under it and counts the bytes.
sealed class CountingStream(Stream inner) : Stream
{
    public long Written { get; private set; }
    public override void Write(byte[] buffer, int offset, int count) { Written += count; inner.Write(buffer, offset, count); }
    public override void Write(ReadOnlySpan<byte> buffer) { Written += buffer.Length; inner.Write(buffer); }
    public override void Flush() => inner.Flush();
    protected override void Dispose(bool disposing) { if (disposing) inner.Dispose(); base.Dispose(disposing); }
    public override bool CanRead => false;
    public override bool CanSeek => false;
    public override bool CanWrite => true;
    public override long Length => throw new NotSupportedException();
    public override long Position { get => Written; set => throw new NotSupportedException(); }
    public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
}

Output

CSV text: 132,227 characters
into the GZipStream: 132,227 bytes; into the FileStream: 13,002 bytes
read back through FileStream, GZipStream and StreamReader: 10001 lines
disposed too late: 10 bytes, which decompress to 0 lines, and nothing threw
disposed in time:  11,489 bytes, which decompress to 10001 lines
can seek? FileStream True, GZipStream False

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run stack.cs

The StreamWriter turned the text into 132,227 bytes of UTF-8 for the GZipStream, which passed 13,002 compressed bytes to the FileStream. The StreamWriter is not a stream itself but a TextWriter, which turns characters into bytes for the stream under it. The other two derive from Stream, with Read, Write and Seek, and CanRead, CanWrite and CanSeek say which of them a stream supports: the FileStream could seek, the GZipStream could not, since compressed data is read in order. Some streams are where the bytes end up, a FileStream in a file and a MemoryStream in an array; others wrap a stream and change the bytes on their way to it, as a GZipStream compresses them and a BufferedStream gathers them into larger pieces. Step through one Write call down the stack, and the dispose that finishes it:

Interactive · stage by stage One Write call through a StreamWriter, a GZipStream and a FileStream Step through the first part of stack.cs. Each box hands bytes to the one below it; the dispose at the end runs from the top down.
One Write call through a StreamWriter, a GZipStream and a FileStream, step 5 of 5. Showing The CSV text, StreamWriter (encodes into a buffer), GZipStream (last block and footer), FileStream (header and full blocks), The file on disk (a whole gzip file). In focus: The file on disk.The CSV texta string of rowsStreamWriterencodes into a bufferGZipStreamlast block and footerFileStreamheader and full blocksThe file on diska whole gzip file

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. One call. writer.Write(rows) receives the whole CSV as characters. The StreamWriter is not a stream: it turns characters into UTF-8 bytes and passes them to the stream below it.
  2. Bytes into the GZipStream. The encoded bytes reach the GZipStream in pieces. The counting stream above it saw every one of them, the same number of bytes as the UTF-8 text.
  3. Compressed bytes into the file. The GZipStream writes a header and compressed blocks to the FileStream as each block fills, but keeps its newest output, the tail of the data, until it is told that nothing more is coming.
  4. The using block ends. Disposing the StreamWriter flushes its last characters and disposes the GZipStream, because the writer owns it. Now the GZipStream writes its last block and its footer.
  5. The file is complete. The GZipStream disposes the FileStream it owns, which closes the file. Only now does the file hold a whole gzip stream, and reading it back gives every line.

A compressing stream keeps the tail of its output until it learns that nothing more is coming, and only Dispose tells it. Disposing the writer disposes the streams it owns, unless one was made with leaveOpen: true.

Reading the result before the GZipStream is disposed

CompressTooLate declares its GZipStream with using var, which disposes at the end of the method, after output.ToArray() has copied the buffer. It returned the 10-byte gzip header, which decompressed to 0 lines. End the using statement first, as Compress does.

Checkpoint

Check: Stacked streams and disposing them

  1. Question 1 of 2 A method runs: using var gz = new GZipStream(ms, CompressionLevel.Optimal); gz.Write(data); return ms.ToArray(); What does the caller get?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Only the start of a gzip stream: in stack.cs, the 10-byte header

    A compressing stream writes its last block and its footer when it is disposed. Read the result only after the using statement has ended.

  2. Question 2 of 2 A method is given a Stream by its caller and writes compressed data into it. What must it pass when it creates the GZipStream?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: leaveOpen: true, so disposing the GZipStream does not dispose the caller's stream

    Wrapping streams own the stream they wrap unless leaveOpen is true. Pass it whenever the inner stream is not yours to close, or you need it afterwards.

GZip is the format every gzip tool reads, Brotli makes text the smallest

sizes.cs compresses the same CSV with the four compressed formats of System.IO.Compression, at three levels:

sizes.cs: GZip, Brotli, ZLib and Deflate at three levels C# · sizes/sizes.cs
// The same CSV compressed with GZip, Brotli, ZLib and Deflate at three levels, and each result decompressed again
// to check that nothing changed.
using System.IO.Compression;
using System.Text;

byte[] csv = Encoding.UTF8.GetBytes(MakeCsv(10_000));
Console.WriteLine($"CSV: {csv.Length:N0} bytes");
Console.WriteLine($"{"level",-13}{"GZip",9}{"Brotli",9}{"ZLib",9}{"Deflate",9}");
foreach (CompressionLevel level in new[] { CompressionLevel.Fastest, CompressionLevel.Optimal, CompressionLevel.SmallestSize })
{
    int gzip = Size(s => new GZipStream(s, level));
    int brotli = Size(s => new BrotliStream(s, level));
    int zlib = Size(s => new ZLibStream(s, level));
    int deflate = Size(s => new DeflateStream(s, level));
    Console.WriteLine($"{level,-13}{gzip,9:N0}{brotli,9:N0}{zlib,9:N0}{deflate,9:N0}");
}
byte[] packed = Compress(s => new GZipStream(s, CompressionLevel.Optimal));
Console.WriteLine($"GZip Optimal is {(double)csv.Length / packed.Length:F1} times smaller; it starts with {packed[0]:X2} {packed[1]:X2} and decompresses to {Decompress(packed).Length:N0} bytes, same bytes: {Decompress(packed).AsSpan().SequenceEqual(csv)}");

// The same bytes handed to the compressor 1,024 at a time, as a StreamWriter with its default buffer hands them over.
var pieces = new MemoryStream();
using (var z = new GZipStream(pieces, CompressionLevel.Optimal, leaveOpen: true))
    for (int at = 0; at < csv.Length; at += 1024) z.Write(csv, at, Math.Min(1024, csv.Length - at));
Console.WriteLine($"GZip Optimal, the CSV written in pieces of 1,024 bytes: {pieces.Length:N0} bytes");

int Size(Func<Stream, Stream> compressor) => Compress(compressor).Length;

byte[] Compress(Func<Stream, Stream> compressor)
{
    var output = new MemoryStream();
    using (Stream z = compressor(output)) z.Write(csv);
    return output.ToArray();
}

static byte[] Decompress(byte[] gzip)
{
    var output = new MemoryStream();
    using (var z = new GZipStream(new MemoryStream(gzip), CompressionMode.Decompress)) z.CopyTo(output);
    return output.ToArray();
}

static string MakeCsv(int count)
{
    var text = new StringBuilder("from,to,fare\n");
    string[] stations = ["PUNE", "NDLS", "MAS", "HWH", "SBC", "ADI"];
    for (int i = 0; i < count; i++)
        text.Append(stations[i % 6]).Append(',').Append(stations[(i * 5 + 1) % 6]).Append(',').Append(200 + (i * 37) % 1800).Append('\n');
    return text.ToString();
}

Output

CSV: 132,227 bytes
level             GZip   Brotli     ZLib  Deflate
Fastest         25,248    4,635   25,236   25,230
Optimal         11,489    2,596   11,477   11,471
SmallestSize     5,778    2,342    5,766    5,760
GZip Optimal is 11.5 times smaller; it starts with 1F 8B and decompresses to 132,227 bytes, same bytes: True
GZip Optimal, the CSV written in pieces of 1,024 bytes: 12,996 bytes

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run sizes.cs

GZipStream writes the .gz format of RFC 1952: Deflate data in a frame whose footer holds a CRC-32 and the length. Its output starts with the bytes 1F 8B, which is how tools recognise a .gz file. ZLibStream and DeflateStream write the same data in a smaller frame or none. BrotliStream, the br format of RFC 7932, made the CSV the smallest, 2,596 bytes against GZip’s 11,489 at Optimal. SmallestSize spends more time for fewer bytes, Fastest the reverse, and how you feed a compressor counts too: the CSV written to GZip in pieces of 1,024 bytes made 12,996 bytes, close to the 13,002 of stack.cs, whose writer also hands its text over in pieces. A ZipArchive keeps several named files in one .zip. Answer two questions about your own output:

Interactive · answer and compare Which compressed format fits your output? Answer two questions about your output. Formats that cannot do the job are ruled out, with the reason.
Who reads what you write?
One stream of data, or several files?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four compressed formats against who can read them, the size of the CSV and whether they hold several files
Format Who can read it Size of the CSV Several files in one
GZip .NET type: GZipStream Good .gz files of RFC 1952: gzip tools and gzip HTTP responses Mixed 11,489 bytes at Optimal Poor one stream of data, no names
Brotli .NET type: BrotliStream Mixed the br format of RFC 7932: HTTP clients and servers Good 2,596 bytes at Optimal, the smallest Poor one stream of data, no names
ZIP archive .NET type: ZipArchive Good .zip archives, opened by many tools on every system Mixed Deflate inside each entry, so close to GZip Good named entries in folders, each with its size
ZLib or Deflate .NET type: ZLibStream, DeflateStream Poor formats used inside other formats, not files people open Mixed 11,477 and 11,471 bytes at Optimal, the same data as GZip Poor one stream of data, no names
How the answers decide
  • “One stream of data, or several files?” Several files: rules out GZip, Brotli and ZLib or Deflate. Only a ZIP archive keeps several named files, with their folders, in one file.
  • “Who reads what you write?” People and any tool: rules out ZLib or Deflate and adds a caveat to Brotli. ZLib and Deflate are building blocks of other formats, and fewer tools open Brotli than open GZip.
  • “Who reads what you write?” Browsers or HTTP clients and “One stream of data, or several files?” One stream: adds a caveat to GZip. HTTP clients accept both gzip and br; Brotli made the CSV the smallest, GZip is the one every client knows.
  • “Who reads what you write?” Browsers or HTTP clients and “One stream of data, or several files?” One stream: rules out ZIP archive and adds a caveat to ZLib or Deflate. HTTP compresses a response body with a content coding such as br, gzip or deflate, the zlib format, which RFC 9110 notes some implementations send without its frame. A ZIP archive is not one.
  • “Who reads what you write?” Only your own code and “One stream of data, or several files?” One stream: adds a caveat to ZIP archive. For one stream of data a ZIP archive only adds a list of entries; any of the stream formats will do.

Key idea

GZip for output that any tool may read, Brotli when the reader accepts it and the data is text, a ZIP archive for several files; the levels trade compression time for size.

Checkpoint

Check: GZip, Brotli and ZIP compared

  1. Question 1 of 2 In sizes.cs, at CompressionLevel.Optimal, which format made the 132,227-byte CSV the smallest?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Brotli, 2,596 bytes against 11,489 for GZip

    GZip, ZLib and Deflate hold the same compressed data in different frames. Brotli is a different algorithm and made this text far smaller.

  2. Question 2 of 2 A user downloads a month of daily reports, 30 CSV files, as one file. Which format fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A ZIP archive, written with ZipArchive

    The stream formats compress one sequence of bytes. Several files need an archive, and ZipArchive writes one with a named entry per file.

System.IO.Pipelines lends you buffers instead of filling yours

A StreamReader reading a 200 MB log makes a string for every line, and each one is garbage as soon as its line is parsed. With System.IO.Pipelines, a PipeReader: The reading end of System.IO.Pipelines. It reads a stream into buffers rented from a pool, returns the bytes not yet used, and is told with AdvanceTo how many were used and how many were looked at. reads into buffers rented from a pool, hands you every byte you have not used yet, and AdvanceTo tells it how far you got. Step through one turn:

Interactive · message by message One turn of a PipeReader loop Step through one ReadAsync and one AdvanceTo. Your loop is on the left, the PipeReader in the middle and the stream it reads on the right.
One turn of a PipeReader loop, step 6 of 6. Messages so far: Your loop to PipeReader: ReadAsync(); PipeReader to The stream: read into a pooled buffer; PipeReader to Your loop: ReadResult: the bytes; Your loop to PipeReader: AdvanceTo(consumed, examined); PipeReader to Your loop: half line plus new bytes.Your loopwhole lines parsedPipeReaderkeeps the half lineThe streama file, gzip or socketReadAsync()read into a pooled bufferReadResult: the bytesAdvanceTo(consumed, examined)half line plus new bytes

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Ask for data. The loop calls ReadAsync on the PipeReader. It does not pass a buffer of its own: the reader owns all the memory.
  2. The reader fills a pooled buffer. The PipeReader rents a buffer from a pool and reads bytes from the stream into it. Nothing new is allocated for the data itself.
  3. Everything not yet used. ReadAsync returns a ReadOnlySequence of bytes: everything that has arrived and that the loop has not yet said it used. It can span several buffers.
  4. Parse where they lie. The loop finds the whole lines in the sequence and parses them in place. The last bytes are half a line, cut off where the read ended, so the loop leaves them.
  5. Say how far you got. AdvanceTo tells the reader two positions: consumed, the end of the last whole line, and examined, everything the loop has looked at. The reader can now reuse the memory of the consumed bytes.
  6. The next turn. The next ReadAsync waits for new bytes, then returns the half line followed by them, so the line that was cut in two is whole again. The loop never copied it.

The loop never owned a buffer, so it never had to copy a half line to the front of one, or grow one for a long line: the pipe keeps every byte it was not told to drop. A SequenceReader reads through those bytes in place, as a ReadOnlySequence<T>: A run of values that may lie in several buffers one after another, as a PipeReader returns bytes. A SequenceReader<T> reads through it in place, across the buffer boundaries. that may span several buffers. System.IO.Pipelines is part of the shared framework, so no package is needed.

Checkpoint

Check: One turn of a PipeReader loop

  1. One question A loop has parsed every whole line of the buffer and left half a line at its end. What does it tell the PipeReader with AdvanceTo?

    Choose one answer.

    Show the answer: One question

    Answer: That it consumed up to the end of the last whole line and examined everything

    AdvanceTo takes two positions: what was consumed, whose memory the pipe may reuse, and what was examined, which decides whether the next read waits for new data.

Truncated and oversized compressed input: failures reproduced, and the checks that catch them Pro: opens with a Pro, Premium or Ultimate pass

Compressed input from outside can be broken in ways that raise no exception at all. This part reproduces a file cut in half, a small file that grows a thousandfold and a ZIP archive with an entry that leaves its folder, and builds the check that stops each one.

In this part:

  • A gzip file cut in half decompresses without an error
  • A thousandfold expansion, stopped by a limit on the output
  • A ZIP entry that climbs out of its folder

2 runnable examples (C#) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

A 200 MB log through StreamReader and PipeReader, measured, with the mistake that loses a line Pro: opens with a Pro, Premium or Ultimate pass

The step-through above showed one turn of a PipeReader loop. This part runs a whole loop over a 200 MB log against StreamReader.ReadLine, measures what each allocates, and reproduces the most common mistake of such loops.

In this part:

  • The same log read three ways, measured
  • What consumed and examined mean, and the loop that loses its last line
  • Three settings that decide how a pipe behaves

1 runnable example (C#) · about 4 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

A lab: gzip CSV to gzip NDJSON in one pass, inside a memory budget Pro: opens with a Pro, Premium or Ultimate pass

Each part of this lesson showed one piece: stacked streams, compression, a pipe loop. This lab joins them into one converter that reads compressed CSV and writes compressed NDJSON, and checks it against a memory budget.

In this part:

  • The converter and its six checks
  • How the budget is met, piece by piece

1 runnable example (C#) · about 4 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise

Three ideas of this lesson are checked below, each in a few questions: stacking and disposing streams, choosing a format, and the turn of a pipe loop.

Practice that adapts to you

Practise this lesson

7 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 7 A using block ends and disposes a StreamWriter that wraps a GZipStream over a FileStream. Which of them are disposed?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: All three: each disposes the stream it wraps, unless one was made with leaveOpen: true

    Disposing the outermost object disposes everything under it, which is also when the GZipStream writes its last block and footer.

  2. Question 2 of 7 stack.cs printed CanSeek False for its GZipStream. What follows for code that reads one?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: It must read the data in order, from the start; it cannot jump to a position

    CanRead, CanWrite and CanSeek say which operations a stream supports. Check them before relying on Seek, Length or Position.

  3. Question 3 of 7 Which two bytes started the GZip output in sizes.cs?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: 1F 8B, the start of the .gz format

    The .gz format of RFC 1952 starts with 1F 8B and ends with a CRC-32 and the length of the data.

  4. Question 4 of 7 For GZip, SmallestSize made 5,778 bytes and Fastest 25,248. What does SmallestSize cost in exchange?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: More time to compress

    Choose SmallestSize for data written once and read often, Fastest when compression must keep up with the data, and Optimal as a balance.

  5. Question 5 of 7 A service sends large JSON responses to HTTP clients that accept both gzip and br. Which format gives the smallest responses?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: Brotli, the br format, which made the CSV less than a quarter of GZip's size

    When the reader accepts it, Brotli usually makes text the smallest; GZip remains the choice when you cannot know what the reader supports.

  6. Question 6 of 7 Where does the memory that a PipeReader hands to your loop come from?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: From a pool: the reader rents buffers and reuses them once you have consumed their bytes

    The pipe manages buffers on your behalf, which is also why you must tell it with AdvanceTo when you are done with them.

  7. Question 7 of 7 Why does ReadAsync return a ReadOnlySequence<byte> rather than a single array?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: The bytes not yet consumed can lie in several pooled buffers, one after another

    A SequenceReader walks such a sequence in place, across the buffer boundaries, so your parser never copies the data into one array.

Interview questions

Fresher · What does leaveOpen do when you create a GZipStream or a StreamReader?

By default a wrapping stream owns the stream it wraps: disposing a GZipStream or a StreamReader also disposes the stream under it. leaveOpen: true changes that, so the inner stream stays open and usable. You need it when the inner stream is not yours to close, such as a stream your caller passed in, or when you still need it afterwards, such as a MemoryStream whose bytes you read after the GZipStream has been disposed and has written its footer.

  1. Mid to senior

    A partner's .NET importer reads your nightly export, a .csv.gz file of about 2 million rows. On some nights it imports far fewer rows than the export holds, and nothing on either side logs an error. How would you find the cause, and what would you change so that a short file can never pass as a complete one?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A service accepts ZIP uploads of invoices and extracts them with ZipFile.ExtractToDirectory into a shared folder. One upload of 40 KB filled the disk, and another left files in the folder although its extraction failed. Explain both and redesign the extraction.

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    A service parses 2 GB log uploads with StreamReader.ReadLine and string.Split. While an upload is parsed, the p99 latency of every other request jumps, and the traces show long garbage-collection pauses. How would you rework the parser, and how would you prove that the change helped?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Streams stack: a writer turns text into bytes and each wrapping stream passes them on, and a GZipStream writes its last block and footer only when disposed, so read the result after the using statement ends.
  • GZip’s .gz format is the one tools read everywhere, Brotli made the CSV smallest, and a ZipArchive holds several named files.
  • A PipeReader lends you pooled buffers and AdvanceTo says how far you got, so a parser works on bytes where they lie.

Terms in this lesson

PipeReader
The reading end of System.IO.Pipelines. It reads a stream into buffers rented from a pool, returns the bytes not yet used, and is told with AdvanceTo how many were used and how many were looked at.
ReadOnlySequence<T>
A run of values that may lie in several buffers one after another, as a PipeReader returns bytes. A SequenceReader<T> reads through it in place, across the buffer boundaries.
stream
A sequence of bytes read or written in pieces through the Stream class. FileStream and MemoryStream hold the bytes; wrapping streams such as GZipStream change them on their way to the stream they wrap.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.