Jetsonsoft.PipeR
1.0.0
dotnet add package Jetsonsoft.PipeR --version 1.0.0
NuGet\Install-Package Jetsonsoft.PipeR -Version 1.0.0
<PackageReference Include="Jetsonsoft.PipeR" Version="1.0.0" />
<PackageVersion Include="Jetsonsoft.PipeR" Version="1.0.0" />
<PackageReference Include="Jetsonsoft.PipeR" />
paket add Jetsonsoft.PipeR --version 1.0.0
#r "nuget: Jetsonsoft.PipeR, 1.0.0"
#:package Jetsonsoft.PipeR@1.0.0
#addin nuget:?package=Jetsonsoft.PipeR&version=1.0.0
#tool nuget:?package=Jetsonsoft.PipeR&version=1.0.0
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/ChannelReaderhub 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 | Versions 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. |
-
.NETFramework 4.5.2
- Newtonsoft.Json (>= 13.0.3)
-
.NETStandard 2.0
- Newtonsoft.Json (>= 13.0.3)
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 |