Specify bytes before choosing a convenient writer
A binary format needs an agreement about field order, width, signedness and byte order. BinaryWriter supplies defined encodings for its individual operations, but it cannot decide whether those choices match an external protocol. The lesson deliberately writes one unsigned sixteen-bit value followed by one signed thirty-two-bit value. Those choices make a six-byte layout that can be inspected directly. The comparison with BinaryPrimitives then shows how an explicitly chosen big-endian field differs. Think of the byte specification as the contract, and the writer as one implementation of that contract rather than the source of its meaning.
Decode the six-byte little-endian sequence
The hexadecimal value 0x1234 occupies two bytes and appears as 34 12 when written by this BinaryWriter overload. The integer 0x01020304 then occupies four bytes as 04 03 02 01. Converting the memory stream contents into hexadecimal therefore prints 341204030201. After an explicit rewind, the matching BinaryReader reads the unsigned field as decimal 4660 and the integer as decimal 16909060. Finally, a separate two-byte span receives the unsigned field through WriteUInt16BigEndian, producing 1234. The last print is a different representation of the same numeric value, not an additional field appended to the first stream.
A matching round trip is useful but insufficient evidence
A writer and reader that share the same mistaken assumptions can round-trip a value while still disagreeing with another implementation. The visible hexadecimal sequence is therefore an independent check against the intended layout. For a file format or network frame, write down example byte sequences and test both encoding and decoding against them. Also decide whether extra trailing bytes are allowed and how a truncated field is reported. This sample demonstrates fixed numeric fields only; it does not imply that BinaryWriter.Write(string) writes raw UTF-8 without framing. That string overload has its own length-prefixed representation.
Leave the shared stream available for the next stage
The writer's constructor receives leaveOpen: true, so disposing the writer finishes its own lifetime while preserving the memory stream for inspection and reading. The reader likewise leaves the caller-owned stream open. This avoids a subtle ownership failure in which a wrapper closes the shared resource before the next stage begins. It does not eliminate the need to rewind: after writing, the cursor still follows the last byte. Encoding is supplied explicitly to both wrappers even though these numeric operations do not use it to represent their fixed-width integer values. A later text field would make that choice observable.
Separate protocol byte order from machine byte order
Explicit byte-order APIs describe the desired representation independently of the host CPU. That is valuable when the same C# parser runs on a desktop, a mobile device or the embedded runtime. Avoid inferring a protocol from BitConverter output without accounting for its platform byte order. Also avoid serializing a raw managed object's memory as though field layout, padding and object references formed a stable interchange format. The small spans in this lesson contain bytes that the program intentionally constructed. They are not views into the managed object's internal layout and do not require unsafe code or native interop.
Change one numeric contract at a time
First change only the unsigned value from 0x1234 to 0x00FF. Predict the first four hexadecimal characters of both endian representations, then verify that the decimal reader result is still the same number in each intended encoding. For the second exercise, leave the original input values but attempt to interpret the first two little-endian bytes with ReadUInt16BigEndian. Compare the resulting number with the original and explain why decoding succeeded mechanically while violating the format agreement. Keep the remaining integer field untouched so the disagreement has a single identifiable cause.
- Hint for 0x00FF: little-endian prints FF00 and big-endian prints 00FF.
- Hint for the deliberate mismatch: bytes 34 12 interpreted as big-endian represent 0x3412, or 13330.
Describe a complete record boundary
Before extending the sample, specify whether a record contains one fixed header, a byte-counted payload, repeated fields or an end marker. Numeric precision and byte order alone do not tell a parser where one record ends. The framing companion project makes that next step explicit with a version byte, a big-endian length, strict UTF-8 and independent rejection checks. Its exact-read operations handle partial delivery rather than assuming each numeric field arrives in one transport read. This lesson's output is intentionally smaller: it establishes an observable six-byte layout and shows why external compatibility requires more than a successful paired round trip.
- Keep field widths and signedness in the format description.
- Test known byte sequences independently of matching reader-writer code.
- Document wrapper ownership, cursor position and truncation behavior alongside the format.
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 bytes = new MemoryStream();
using (var writer = new BinaryWriter(bytes, Encoding.UTF8, leaveOpen: true)) { writer.Write((ushort)0x1234); writer.Write(0x01020304); }
Console.WriteLine(Convert.ToHexString(bytes.ToArray())); bytes.Position = 0;
using (var reader = new BinaryReader(bytes, Encoding.UTF8, leaveOpen: true)) { Console.WriteLine(reader.ReadUInt16()); Console.WriteLine(reader.ReadInt32()); }
byte[] networkOrder = new byte[2]; BinaryPrimitives.WriteUInt16BigEndian(networkOrder, 0x1234); Console.WriteLine(Convert.ToHexString(networkOrder));
Expected output
341204030201 4660 16909060 1234
Document byte order and field widths; a matching local writer/reader can agree on the wrong external format.