Table of Contents

Named Pipe Transport

DotBoxD.Transports.NamedPipes provides first-class named-pipe client and server transports for local process-boundary IPC. It is a separate package so hosts that only need TCP or custom transports do not take a named-pipe dependency.

Install

dotnet add package DotBoxD.Transports.NamedPipes

The package depends on DotBoxD and reuses StreamConnection for framing. That means named-pipe traffic uses the same length validation, serialized sends, pooled receive buffers, and clean EOF behavior as every other stream-backed DotBoxD connection.

Host And Caller

A RpcHost turns every accepted named-pipe connection into an RpcPeer; a caller wraps a connected pipe in its own RpcPeer. The generated Provide.../Get... extension methods replace the old builder AddX/CreateXProxy calls.

using DotBoxD.Services;
using DotBoxD.Services.Generated;
using DotBoxD.Codecs.MessagePack;
using DotBoxD.Transports.NamedPipes;

var pipeName = "my-plugin-host";

await using var host = RpcHost
    .Listen(new NamedPipeServerTransport(pipeName), new MessagePackRpcSerializer())
    .ForEachPeer(peer => peer.ProvidePluginHost(new PluginHost()));
await host.StartAsync();

await using var clientTransport = new NamedPipeClientTransport(pipeName);
await clientTransport.ConnectAsync();
await using var peer = RpcPeer
    .Over(clientTransport.Connection!, new MessagePackRpcSerializer(),
          new RpcPeerOptions { RequestTimeout = TimeSpan.FromSeconds(5), RejectInboundCalls = true })
    .Start();

var pluginHost = peer.GetPluginHost();
await pluginHost.PingAsync();

RejectInboundCalls = true signals a get-only intent — the caller does not provide any service of its own. Drop it (and add Provide... calls) when the caller also needs to be called back over the same pipe. The host can react to lifecycle events via host.PeerConnected, host.PeerDisconnected, and host.AcceptError; stop it with await host.StopAsync() (disposal also stops it).

Use the (serverName, pipeName) client constructor when connecting to a remote Windows machine:

var transport = new NamedPipeClientTransport("build-agent-01", "my-plugin-host");

Duplex Peers

Named-pipe streams are duplex, so a single RpcPeer over one pipe connection can both serve and call services when both processes need to talk over the same authenticated pipe. The transports already hand back an IRpcChannel (clientTransport.Connection! / serverConnection from AcceptAsync); wrap a raw PipeStream in StreamConnection only when you manage the stream yourself. Each side calls Provide... for what it serves and Get... for what it calls:

using DotBoxD.Services;
using DotBoxD.Services.Transport;
using DotBoxD.Services.Generated;
using DotBoxD.Codecs.MessagePack;

Stream pipeStream = /* connected PipeStream */;
await using IRpcChannel connection = new StreamConnection(pipeStream, "pipe://plugin-host");

await using var peer = RpcPeer
    .Over(connection, new MessagePackRpcSerializer(),
          new RpcPeerOptions
          {
              RequestTimeout = TimeSpan.FromSeconds(5),
              InboundQueueCapacity = 256,
              QueueFullMode = QueueFullMode.Wait,
          })
    .ProvidePluginHost(new PluginHost())
    .Start();

var plugin = peer.GetPluginCallbacks();

For full symmetry both processes do the same thing — each wraps its end of the pipe in an RpcPeer, provides its own service, and gets a proxy to the other side over the one connection. Set InboundQueueCapacity (or null for unbounded) and QueueFullMode to bound how queued inbound requests are handled under pressure, and raise MaxConcurrentInboundDispatch above the default 1 for bounded-concurrent dispatch instead of strict serial-per-connection handling.

RpcPeer.Disconnected reports the endpoint and the closing exception, and RpcPeer.ReadError surfaces read-loop failures with endpoint and error details. On a host, subscribe per peer inside ForEachPeer (for example peer.ReadError += ...) or watch host.PeerDisconnected for the aggregate signal.