# Nerdbank.Streams: the stream types the .NET base library leaves out

> Nerdbank.Streams is a .NET Foundation supported library of specialised Stream and pipe types, covering one direction and full duplex pairs, multiplexing many protocols over one transport, and bridging between Stream and the Pipelines API. It is MIT licensed, ships on NuGet, and is maintained by the dotnet organisation.

**dotnet/Nerdbank.Streams** — Specialized .NET Streams and pipes for full duplex in-proc communication, web sockets, and multiplexing

- Repository: https://github.com/dotnet/Nerdbank.Streams
- Website: https://dotnet.github.io/Nerdbank.Streams/
- Stars: 723 · Forks: 76
- Language: C#
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/dotnet-nerdbank-streams

## Fifteen specialised types the base library does not ship

Nerdbank.Streams describes itself as enhanced streams for communication in-process or across the internet, and the readme is essentially a catalogue of the gaps in System.IO that it fills.

The list starts with the communication primitives. SimplexStream allows two parties to communicate in one direction, where anything written to the stream can subsequently be read from it, and it can be shared with any two parties in the same AppDomain so one can send messages to the other. FullDuplexStream creates a pair of bidirectional streams for in-process two-way communication, and it can also create a single bidirectional stream from two unidirectional ones, which is how you retrofit duplex behaviour onto a protocol that only gives you one direction each way.

MultiplexingStream is the one that changes an architecture. It lets you split any bidirectional .NET Stream into many sub-streams called channels, so two parties establish one transport stream, a named pipe or a web socket say, and use it for many independent protocols. The example given is running JSON-RPC on one channel while other channels carry efficient binary transfers.

Then there are the bridging and framing types. AsStream() wraps a WebSocket, a PipeReader, a PipeWriter or an IDuplexPipe with a Stream for reading and/or writing. UsePipe() goes the other way, letting you read from and write to a Stream or WebSocket using the PipeReader and PipeWriter APIs. Stream.ReadSlice(long) creates a sub-stream that ends after a given number of bytes, and PipeReader.ReadSlice(long) does the same for a PipeReader.

The rest are smaller utilities: MonitoringStream, which wraps another stream and raises events for all I/O calls so you can trace data as it goes by, WriteSubstream and ReadSubstream for serialising data of unknown length inside a larger stream, Sequence<T> as a builder for ReadOnlySequence<T>, PrefixingBufferWriter<T> for prefixing a header to the next written buffer, BufferTextWriter and SequenceTextReader for allocation free text reading and writing, DuplexPipe as a trivial IDuplexPipe implementation, and Stream.ReadBlockAsync, which guarantees to fill the supplied buffer under certain documented conditions rather than the regular ReadAsync guarantee of at least one byte.

## Why Stream and Pipe do not compose cleanly on their own

The reason this library exists is a specific impedance mismatch in the .NET IO stack, and it is worth understanding before picking a type from the list.

Stream is a pull model with a read call and a write call, and it predates async work in the framework. The Pipelines API, built on PipeReader and PipeWriter, is a push model with an explicit buffer and back pressure. Every application that wants to use modern I/O ends up bridging between the two, and that bridging is where the awkward code lives.

The two directions of the bridge are therefore first class in the library. AsStream() takes the modern types and exposes them as a Stream. UsePipe() takes a Stream or a WebSocket and lets you use the PipeReader and PipeWriter APIs against it. Once you have that pair, the rest of the library, the slices, the sub-streams, the monitoring wrapper, all work in whichever world you have chosen.

Two details in the catalogue reveal where the sharp edges are. ReadSlice exists because reading a length-prefixed message from a plain Stream means counting bytes yourself, and a slice that ends after a given number of bytes gives you that framing in one call. ReadBlockAsync exists because the base ReadAsync contract only promises at least one byte, and code that assumed a full buffer would be wrong, so this version guarantees filling the supplied buffer except under documented conditions.

The text types follow the same logic. BufferTextWriter writes directly to any IBufferWriter<byte> and is more reusable than StreamWriter, which allows allocation free writing across many writers. SequenceTextReader reads directly from any ReadOnlySequence<byte> and is more reusable than StreamReader, allowing allocation free reading across many sequences.

## Multiplexing replaces several transports with one

MultiplexingStream deserves its own look because it is the feature that removes infrastructure rather than adding it.

The problem it solves: if two subsystems need to talk over a boundary and they speak different protocols, the usual answers are separate connections or a protocol multiplexer bolted onto one of them. With MultiplexingStream, you open one transport, a named pipe between two processes or a web socket across a network, and then open logical channels inside it.

Each channel is a Stream, so the protocol on top of it does not know or care that it is multiplexed. The readme's example puts JSON-RPC on one channel and binary transfers on others, and both see what looks like a normal bidirectional stream. That decoupling is the point: the two protocols evolve independently, and a slow bulk transfer does not have to share a queue with request and response traffic.

There is a cost to it, and the library is not hiding it. A channel boundary means framing has to be established across the shared transport, and every channel's traffic shares the same underlying bytes, so one channel that writes a very large message can delay another. For a bulk transfer channel that is fine, since you would probably give it its own transport anyway, but it is a real property of the design rather than a free lunch.

The pairing with FullDuplexStream is also sensible. If you already have two unidirectional streams, for example the request and response halves of some protocol, FullDuplexStream turns them into one bidirectional stream, and MultiplexingStream can then split that in turn. The two features compose in the direction you would expect.

## A .NET Foundation project with a NuGet package and a busy release train

The maintenance and governance signals here are strong, and the packaging follows the .NET Foundation conventions.

The project is supported by the .NET Foundation, the licence is MIT with the LICENSE and NOTICE files at the root, and the package is published on NuGet as Nerdbank.Streams, with the readme linking the package and the API documentation hosted on the dotnet GitHub Pages site under the Nerdbank.Streams docs path.

The release train is the notable part. The three most recent versions are v2.14.408 from 2026-10-19, v2.14.405 from 2026-10-13, and v2.15.4-alpha from 2026-10-01. Two things are going on at once. The stable line is on 2.14 and shipping patch-level releases roughly weekly, and the 2.15 line exists in parallel as an alpha, which is how a .NET library stages the next minor version without breaking consumers of the current one.

The numbering is unusual and worth noting: the stable version has a three-part build number like 2.14.408, where the third component is far larger than a typical patch counter. That is a build-number convention some .NET projects use to keep a monotonic counter across many CI builds, and it means the last segment should not be read as a meaningful patch level.

The repository is not archived, the default branch is main, and the last push was on 2026-10-19, the same day as the newest stable release. The top-level layout shows the .NET tooling in place, with Directory.Build.props, Directory.Build.targets and Directory.Packages.props for centralised build settings, a version.json for the version, a solution file in the newer .slnx format, a stylecop.json for the style analyser, docfx/ for the documentation site, a spec/ directory, a test/ directory and a devcontainer configuration.

## Adding the package to a project

There is no install script here, because the delivery is a NuGet package rather than a tool. The package is published as Nerdbank.Streams, and the readme links it on nuget.org alongside a badge showing the current version.

In a .NET project you add it the way you add any package reference, and the class you reach for first depends entirely on the problem in front of you rather than on a recommended starting point. If two components in one process need to talk to each other in both directions, FullDuplexStream gives you the pair. If one only writes and the other only reads, SimplexStream is enough. If several protocols have to share a single pipe or socket, MultiplexingStream is the one, and you open channels inside it. If you already have a Stream and want to use the Pipelines API against it, or the other way round, that is what AsStream and UsePipe are for.

The API documentation is published on the project's GitHub Pages site, with a separate page per type, and each class in the catalogue links to its own page from the readme. That is where the exact constructor and method signatures live, and it is the fastest way to confirm which of the overlapping types actually fits your transport.

The samples are not shipped in the readme, but the repository carries a test/ directory, a spec/ directory and a devcontainer setup, and the build configuration shows a centralized package version file, so the project follows current .NET conventions for dependency management.

## Where this library is the wrong choice

There is a class of project where reaching for Nerdbank.Streams is a mistake, and it is worth naming so the library does not get adopted by reflex.

If you need a file stream, a network stream or a memory stream for a single producer and single consumer, the base library already does it, and adding a dependency to get the same thing in a more elaborate package is not a trade. The library's value is concentrated in the awkward cases: two-way in-process communication, multiplexing, bridging, and framing.

If you need message delivery guarantees, it is not that either. Nerdbank.Streams gives you channels, framing and duplex plumbing. It does not give you persistence, redelivery, consumer groups, or ordering across restarts. A team that needs a queue should look at a messaging system, and if that system happens to expose a Stream or a Pipe at its edges, then this library is a good fit for that seam and a bad fit for the layer above it.

There is a third boundary, which is the dependency on understanding the underlying transport. MultiplexingStream over a web socket inherits the web socket's characteristics, including the fact that a single transport connection is a single point of failure, and the library does not add reconnection logic to the channel layer. Likewise the Substream helpers are about framing bytes within a stream you control, not about surviving a broken connection.

The pragmatic summary is that this is infrastructure for people who already know which awkward IO problem they have. If you cannot name the specific type you need from the catalogue, you probably do not need the library yet.

## Conclusion

Nerdbank.Streams suits a .NET developer who needs full duplex communication in one process, several protocols over a single named pipe or web socket, or a clean bridge between the Stream and Pipelines APIs without writing that plumbing by hand. It does not suit a project that only needs a file or network stream, since the base library already covers that, and it is a low level library rather than a messaging system, so it gives you channels and framing, not delivery guarantees. Before adopting it, install the Nerdbank.Streams package from NuGet, decide whether you need a duplex pair or a multiplexing stream, and read the readme for the class that matches your transport.

## FAQ

### What is Nerdbank.Streams?

It is a library of specialised .NET Stream and pipe types for communication in-process or across the internet, published on NuGet as Nerdbank.Streams and supported by the .NET Foundation under the MIT licence.

### What does MultiplexingStream do?

It splits any bidirectional .NET Stream into many sub-streams called channels, so two parties establish one transport stream, a named pipe or web socket for example, and use it for several independent protocols. One channel could carry JSON-RPC while others carry efficient binary transfers.

### How do I bridge between Stream and the Pipelines API?

AsStream() wraps a WebSocket, PipeReader, PipeWriter or IDuplexPipe with a Stream for reading and writing, while UsePipe() lets you read from and write to a Stream or WebSocket using the PipeReader and PipeWriter APIs.

### What is the difference between ReadAsync and Stream.ReadBlockAsync?

The regular ReadAsync only guarantees supplying at least one byte. Stream.ReadBlockAsync in this library guarantees to fill the supplied buffer except under certain documented conditions, which is what code that assumes a full buffer actually needs.

### How do I monitor traffic on a stream?

MonitoringStream wraps another Stream and raises events for all I/O calls, so you can monitor and trace the data as it goes by. The library also has WriteSubstream and ReadSubstream for framing data of unknown length inside a larger stream.

## Sources

- [dotnet/Nerdbank.Streams on GitHub](https://github.com/dotnet/Nerdbank.Streams)
- [License: MIT](https://github.com/dotnet/Nerdbank.Streams/blob/main/LICENSE)
- [Project website](https://dotnet.github.io/Nerdbank.Streams/)
- [README](https://github.com/dotnet/Nerdbank.Streams/blob/main/README.md)
- [Releases](https://github.com/dotnet/Nerdbank.Streams/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dotnet-nerdbank-streams
