TagBites.Pipes 1.2.1

Prefix Reserved
dotnet add package TagBites.Pipes --version 1.2.1
                    
NuGet\Install-Package TagBites.Pipes -Version 1.2.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TagBites.Pipes" Version="1.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TagBites.Pipes" Version="1.2.1" />
                    
Directory.Packages.props
<PackageReference Include="TagBites.Pipes" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TagBites.Pipes --version 1.2.1
                    
#r "nuget: TagBites.Pipes, 1.2.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TagBites.Pipes@1.2.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TagBites.Pipes&version=1.2.1
                    
Install as a Cake Addin
#tool nuget:?package=TagBites.Pipes&version=1.2.1
                    
Install as a Cake Tool

TagBites.Pipes

Nuget .NET Standard 2.1 License Downloads

TagBites.Pipes sends requests between two processes on the same machine, over a named pipe. The server handles requests by address, and the client calls them and gets back the answer, or the exception the handler threw. A payload can be a string, a byte array, or a stream that never has to fit in memory.

// server
using var server = new NamedPipeServer("my-pipe");
server.Request += (_, e) => e.Response = $"Hello, {e.Message}!";
server.Enabled = true;

// client
using var client = new NamedPipeClient("my-pipe");
await client.ConnectAsync();

await client.SendRequestAsync("greet", "world"); // "Hello, world!"

On top of System.IO.Pipes it adds what a request and response protocol needs: message boundaries, matching every answer to its question, and one task per connected client.

Install

dotnet add package TagBites.Pipes

Targets netstandard2.1, has no dependencies and runs on Windows, Linux and macOS. A named pipe is local, so both processes live on the same machine.

Usage

Serving requests

The server accepts nothing until Enabled is set. Address says what to do and Message carries the payload.

using var server = new NamedPipeServer("my-pipe");
server.Request += (_, e) => e.Response = e.Address switch
{
    "greet" => $"Hello, {e.Message}!",
    "time" => DateTime.Now.ToString("O"),
    _ => null
};
server.Enabled = true;

The handler runs on a thread pool thread with no synchronization context, so code that touches a user interface has to marshal itself.

To answer asynchronously, hand the server a task and set the response when it finishes.

server.Request += (_, e) => e.ResultTask = HandleAsync(e);

static async Task HandleAsync(NamedPipeRequestEventArgs e)
{
    e.Response = await File.ReadAllTextAsync(e.Message);
}

Sending requests

using var client = new NamedPipeClient("my-pipe");
await client.ConnectAsync();

var greeting = await client.SendRequestAsync("greet", "world"); // "Hello, world!"
var time = await client.SendRequestAsync("time", "");

Every call also has a synchronous form, Connect and SendRequest. Connect waits 100 milliseconds by default and throws TimeoutException when no server answers. The overload takes a longer timeout and a cancellation token.

await client.ConnectAsync(5000, CancellationToken.None);

Sending from several threads

One client serves one request at a time. NamedPipeClientPool keeps a set of connections and hands them out, so several threads can send at once. A caller waits while every connection is busy.

using var pool = new NamedPipeClientPool("my-pipe", 4);

var greeting = await pool.SendRequestAsync("greet", "world");

To keep one connection for a few requests in a row, take a link and dispose it when done.

using (var link = await pool.GetConnectionAsync())
{
    await link.SendRequestAsync("open", "file.txt");
    await link.SendRequestAsync("read", "100");
}

Large payloads

Sending megabytes as a string means base64 for anything binary, and a full copy of the payload on each side. Streams avoid both and work in constant memory, whatever the size.

Turn on UseMessageStream on the server, then read the request and write the answer as streams.

using var server = new NamedPipeServer("my-pipe") { UseMessageStream = true };
server.Request += (_, e) => e.ResultTask = HandleAsync(e);
server.Enabled = true;

static async Task HandleAsync(NamedPipeRequestEventArgs e)
{
    var order = await JsonSerializer.DeserializeAsync<Order>(e.MessageStream);
    var receipt = Process(order);

    e.SetResponse(stream => JsonSerializer.SerializeAsync(stream, receipt));
}
var receipt = await client.SendRequestAsync("submit",
    stream => JsonSerializer.SerializeAsync(stream, order),
    stream => JsonSerializer.DeserializeAsync<Receipt>(stream).AsTask());

For bytes already in memory there is a shorter form.

var thumbnail = await client.SendBytesAsync("resize", File.ReadAllBytes("photo.jpg"));

Two rules come with the stream form. Read MessageStream inside the handler and not inside the response callback, because the request is consumed before the answer starts. And once the answer starts, a failure breaks the connection instead of reporting an error, the same way an HTTP response cannot be taken back.

Leave UseMessageStream off for handlers that only want Message as a string.

State that lives with a connection

Context identifies the connection and carries a store for values that last as long as it does. Assigning null removes an entry, and Disposing fires when the client goes away.

server.Request += (_, e) =>
{
    if (e.Address == "login")
        e.Context.Bag["user"] = e.Message;

    e.Response = e.Context.Bag["user"] as string ?? "anonymous";
};

When something fails

An exception in the handler reaches the client as NamedPipeServerRemoteException, carrying the type name, the message and the stack trace as text.

try
{
    await client.SendRequestAsync("read", "missing.txt");
}
catch (NamedPipeServerRemoteException e)
{
    Console.WriteLine(e.RemoteType);       // "System.IO.FileNotFoundException"
    Console.WriteLine(e.Message);          // the message from the server
    Console.WriteLine(e.RemoteStackTrace); // the stack trace from the server
}

Set IncludeExceptionStackTrace to false to keep the stack trace on the server. The type and the message still travel, so a message built from a path or a connection string reaches the caller either way.

A broken connection raises NamedPipeConnectionLostException. Such a request has an unknown outcome, because the server may have handled it before the connection died. The pool drops that connection, so the next request gets a fresh one.

Use cases

  • a desktop application talking to its own background service
  • a command line tool driving a process that is already running
  • splitting work into two processes, for example a plugin host with a different bitness
  • moving large files or JSON documents between two local processes

Why TagBites.Pipes?

  • Request and response, not a raw stream - message boundaries and answer matching are handled.
  • Large payloads in constant memory - stream a request and a response instead of building strings.
  • A failed handler comes back as an exception - with the type, the message and the stack trace from the other process.
  • Runs anywhere .NET runs - Windows, Linux and macOS, with no dependencies.
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.1

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on TagBites.Pipes:

Package Downloads
VendoStandard

Common API for Vendo Lite, Vendo Server and Vendo ERP Desktop projects.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.1 96 10/12/2026
1.2.0 322 7/31/2026
1.1.1 378 6/5/2024
1.1.0 5,815 3/5/2024
1.0.1 329 11/29/2023