Jetsonsoft.PipeR 1.0.0

dotnet add package Jetsonsoft.PipeR --version 1.0.0
                    
NuGet\Install-Package Jetsonsoft.PipeR -Version 1.0.0
                    
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="Jetsonsoft.PipeR" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Jetsonsoft.PipeR" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Jetsonsoft.PipeR" />
                    
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 Jetsonsoft.PipeR --version 1.0.0
                    
#r "nuget: Jetsonsoft.PipeR, 1.0.0"
                    
#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 Jetsonsoft.PipeR@1.0.0
                    
#: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=Jetsonsoft.PipeR&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Jetsonsoft.PipeR&version=1.0.0
                    
Install as a Cake Tool

PipeR

SignalR's programming model over a Windows named pipe.

You write a Hub. Clients build a HubConnection, register handlers with On, and call hub methods with SendAsync and InvokeAsync. The server pushes with Clients.All, Clients.Group(...), Clients.Caller, and reaches its clients from elsewhere in the application through IHubContext<THub>. All of that is SignalR, unchanged.

What is different is underneath: there is no HTTP server, no port, no URL and no negotiate step — just a pipe name both sides know.

// server
public sealed class ChatHub : Hub
{
    public Task Join(string room) => Groups.AddToGroupAsync(Context.ConnectionId, room);

    public Task Say(string room, string text) =>
        Clients.Group(room).SendAsync("Said", Context.UserIdentifier, text);
}

using var server = new HubServer<ChatHub>("contoso.chat");
server.Start();
// client
var connection = new HubConnectionBuilder()
    .WithPipe("contoso.chat")
    .WithAutomaticReconnect()
    .Build();

connection.On<string, string>("Said", (who, what) => Console.WriteLine($"{who}: {what}"));

await connection.StartAsync();
await connection.InvokeAsync("Join", "lobby");
await connection.SendAsync("Say", "lobby", "morning");

Why

Two processes on one machine that need to talk usually reach for SignalR because the model fits — named calls, pushes both ways, groups, reconnection — and then have to stand up a web server to get it. A named pipe carries the same conversation with no listener to configure, no port to open, and no firewall prompt. It is also the transport Windows already trusts between processes: the pipe's ACL decides who may connect, and the server learns the caller's real account from the handle rather than from anything the caller claims.

Installing

dotnet add package Jetsonsoft.PipeR

Targets net452 and netstandard2.0. Newtonsoft.Json is the only dependency.

The server

HubServer<THub> listens, accepts clients, dispatches their calls, and is itself an IHubContext<THub> so the rest of the application can push without knowing about connections.

var server = new HubServer<ChatHub>("contoso.chat", new HubServerOptions
{
    MaxConnections = 64,
    AllowCrossAccountClients = false,
});

server.ClientConnected    += (s, e) => Log($"{e.UserIdentifier} joined as {e.ConnectionId}");
server.ClientDisconnected += (s, e) => Log($"{e.ConnectionId} left: {e.Exception?.Message}");

server.Start();

Hub methods are found by name, case-insensitively, and may take and return anything Json.NET can carry. void, Task and Task<T> all work; a trailing CancellationToken parameter is supplied by the server and is cancelled if the caller goes away mid-call. Overloads are refused when the server is constructed — a client calls by name only, so two methods with one name have no answer.

A hub instance is created per invocation and disposed afterwards, exactly as in SignalR. Per-connection state belongs in Context.Items; state shared by every connection belongs in whatever your hub factory closes over:

var store = new SettingsStore();
var server = new HubServer<SettingsHub>("contoso.settings", () => new SettingsHub(store));

Calls from one client are dispatched one at a time and in order, so a slow hub method holds up that client's later calls — and only that client's.

Errors

A HubException thrown by a hub method reaches the caller with its message intact. Anything else reaches the caller as An unexpected error occurred invoking 'X' on the server., because an unhandled exception's message can carry a path or a connection string and the caller is another process. Set HubServerOptions.DetailedErrors while developing to send the real one.

Who is connecting

Context.UserIdentifier is the Windows account the client process runs as, in DOMAIN\user form, as Windows reports it for the connected pipe. Clients.User(...) and Clients.Users(...) address by it. It is not a claim the client sends, so a hub can use it to decide what a caller may do.

By default the pipe admits any process running as the account that created it, in any logon session — which is the common case, and needs no configuration. AllowCrossAccountClients widens the ACL to authenticated users so another account, including a service account, can connect. Weigh it before turning it on: any logged-on account can then call the hub's methods, and sorting out who may do what becomes the hub's job.

That flag is honoured by the net452 build, which is the one every .NET Framework host resolves. The netstandard2.0 build cannot reference PipeSecurity; supply HubServerOptions.PipeServerFactory there to build the pipe instances with an ACL of your own.

The client

var connection = new HubConnectionBuilder()
    .WithPipe("contoso.chat")                   // or .WithPipe("build-01", "contoso.chat")
    .WithAutomaticReconnect()
    .WithServerTimeout(TimeSpan.FromSeconds(30))
    .Build();

On returns an IDisposable that unregisters the handler. SendAsync calls a hub method without waiting for it; InvokeAsync waits, and turns a hub method's failure into a HubException on the calling side.

Handlers run one at a time on the connection's receive loop, in the order the server sent them. That keeps ordering intact, and it means a handler that blocks holds up everything behind it — including the completion of an InvokeAsync. Never await an InvokeAsync on the same connection from inside a handler.

Reconnecting

Closed, Reconnecting and Reconnected behave as SignalR's do, and so does the retry policy: WithAutomaticReconnect() tries at once, then after 2, 10 and 30 seconds, then gives up. RetryPolicies.Forever(interval) keeps trying instead, which is usually what you want when the server is a desktop app the user starts and stops.

The connection that comes back is a new connection with a new id, and the server remembers nothing about the old one — group membership above all. Re-subscribe in Reconnected:

connection.Reconnected += async _ => await connection.SendAsync("Join", "lobby");

There is one deliberate departure from SignalR. Pass retryInitialConnect: true and the retry policy covers the first connect as well, so StartAsync returns with the connection reconnecting rather than throwing when the server is not running yet:

.WithAutomaticReconnect(RetryPolicies.Forever(TimeSpan.FromSeconds(2)), retryInitialConnect: true)

Two desktop processes are started in whatever order the user likes. Without this, every client has to write the same retry loop around StartAsync.

Keep-alive

Both sides ping when idle (KeepAliveInterval, 15 seconds) and give up on a peer that has said nothing for ServerTimeout / ClientTimeout (30 seconds). This is what catches a peer that hangs rather than exits — the pipe stays open in that case, so silence is the only signal.

On the wire

SignalR's JSON hub protocol: one JSON object per frame, UTF-8, terminated by the ASCII record separator (0x1E). The same type numbers mean the same things — invocation 1, completion 3, ping 6, close 7 — and the handshake is the same request and response, with the connection id added to the response since there is no negotiate step to hand one out.

{"protocol":"json","version":1}0x1E
{"connectionId":"9f2c…"}0x1E
{"type":1,"target":"Say","arguments":["lobby","morning"]}0x1E
{"type":1,"invocationId":"1","target":"Join","arguments":["lobby"]}0x1E
{"type":3,"invocationId":"1"}0x1E

What it does not do

  • Streaming. IAsyncEnumerable/ChannelReader hub methods and client streams have no equivalent; the frame types (2, 4, 5) are reserved and refused rather than misread.
  • Strongly-typed hubs. There is no Hub<T>; Clients.All.SendAsync("Method", …) names the method as a string.
  • Scale-out. One server, one machine, one process. That is the whole point of the transport.
  • Non-Windows transports. Named pipes exist on Unix through .NET, but the identity and ACL parts of this are Windows.

License

MIT.

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 netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net452 is compatible.  net46 was computed.  net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  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.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Jetsonsoft.PipeR:

Package Downloads
Lockjaw.Core

The shared model behind Lockjaw: reads environment-prefixed settings from app.config and appsettings.json, resolves each key to the environment you have selected, and defines the snapshot the Lockjaw app publishes and clients read. Environments are open-ended — Development_ and Production_ are only the defaults, and any prefix you declare (Local_, Test_, Staging_) is picked up. Includes the editable settings catalog, overlay persistence that never rewrites your configuration files, and import/export. Most applications want Lockjaw.Client instead. Reference this package directly only when hosting the resolution yourself or building tooling on top of Lockjaw.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 175 9/10/2026