Every wrapper needs a stated resource owner
A text writer or reader has its own lifetime and may also control the stream it wraps. The leaveOpen constructor option makes that second responsibility explicit. The lesson first creates a writer that borrows a caller-owned memory stream, then a reader that is allowed to close it. This models an ownership handoff without requiring a file or socket. Decide which scope ultimately owns each resource rather than scattering disposals until a program happens to stop throwing. A wrapper's successful completion and the underlying stream's continued usability are separate facts that callers may depend on.
Finish the writer before inspecting its bytes
The writer uses UTF-8 without emitting a preamble and writes AB. Disposing that writer flushes its buffered characters into the memory stream. Because leaveOpen is true, the stream remains readable, so the first printed capability is True. The sample explicitly rewinds before calling ReadByte, which returns decimal 65, the UTF-8 byte for A. This byte inspection happens after the writer's lifetime ends, avoiding ambiguity about characters that might still be buffered. Leaving the stream open by itself would not have forced an active writer to flush or reset the cursor.
Observe the closing wrapper independently
The sample rewinds again and constructs a reader with leaveOpen: false. It reads the text AB, which appears as the third output line. When this reader's scope ends, it also disposes the underlying memory stream. The final capability check therefore prints False. The baseline output is True, 65, AB, False in that order. No read is attempted after the stream is closed, and the outer using later disposing the memory stream again does not reopen it. This is an explicit lifecycle observation rather than an exception-driven discovery of who closed an object.
A using scope is a boundary even when work fails
A using scope ensures disposal when control exits, including exceptional exits. That gives the wrapper a predictable end boundary but does not make every operation within it successful or transactional. Flushing can itself fail for an external destination, and partially written bytes are not automatically erased. This lesson's memory destination removes those environmental failures so ownership is the visible concern. In reusable code, document whether a method borrows or owns a stream supplied by its caller. A method that unexpectedly closes a borrowed input can break unrelated later work even when its own parsing result was correct.
Keep buffering, cursor state and ownership separate
The sample performs two rewinds because the first byte inspection advances the stream and the later reader should start at the beginning. Rewinding is a positioning decision; disposing the writer is a buffering decision; leaveOpen is an ownership decision. Combining all three into a vague instruction to close the wrapper obscures the cause of failures. A real pipeline may stack compression, encoding and buffering wrappers, each with its own leave-open policy. Draw the stack and choose disposal order so finalization reaches the intended underlying destination before that destination closes. Avoid concurrent wrapper access unless the specific API contract supports it.
Vary one lifetime decision and one buffering decision
For the first exercise, change the reader's leaveOpen argument to true while leaving every other line unchanged. Predict the final capability print, then demonstrate that the caller can rewind and read the bytes again before its own scope ends. For the second, restore the original reader policy and move an inspection of the stream's current bytes inside the writer's scope, before explicitly flushing it. Record the observation without treating it as a universal buffer-size guarantee, then call Flush and inspect again. Explain why disposal remains necessary even when an explicit flush has occurred.
- Hint for the reader: leaving the stream open changes the final CanRead observation to True.
- Hint for buffering: Flush advances buffered output to the destination; it does not transfer lifetime ownership or rewind it.
Write a borrowing contract another caller can use
A clear helper contract might say that it writes UTF-8 text, flushes its wrapper before returning, leaves the supplied stream open, and leaves the cursor after the bytes it produced. Another helper might explicitly take ownership and close the source. Either can be valid when stated clearly. Review the original four outputs against these responsibilities rather than memorizing constructor booleans. The lesson uses no native resources beyond framework memory objects, but the ownership discipline scales to HTTP response content and file streams, where a premature close or a leaked owner can affect far more than one local demonstration.
- Flush buffered output before another stage depends on its bytes.
- Choose leaveOpen according to ownership rather than convenience.
- Document the resulting cursor position separately from disposal behavior.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using var stream = new MemoryStream();
using (var writer = new StreamWriter(stream, new UTF8Encoding(false), bufferSize: 128, leaveOpen: true)) { writer.Write("AB"); }
Console.WriteLine(stream.CanRead); stream.Position = 0; Console.WriteLine(stream.ReadByte()); stream.Position = 0;
using (var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 128, leaveOpen: false)) { Console.WriteLine(reader.ReadToEnd()); }
Console.WriteLine(stream.CanRead);
Expected output
True 65 AB False
Flush the writer before consuming its bytes, and do not dispose a borrowed stream accidentally.