PureGate.Socket 1.0.3

Prefix Reserved
There is a newer version of this package available.
See the version list below for details.
dotnet add package PureGate.Socket --version 1.0.3
                    
NuGet\Install-Package PureGate.Socket -Version 1.0.3
                    
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="PureGate.Socket" Version="1.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PureGate.Socket" Version="1.0.3" />
                    
Directory.Packages.props
<PackageReference Include="PureGate.Socket" />
                    
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 PureGate.Socket --version 1.0.3
                    
#r "nuget: PureGate.Socket, 1.0.3"
                    
#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 PureGate.Socket@1.0.3
                    
#: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=PureGate.Socket&version=1.0.3
                    
Install as a Cake Addin
#tool nuget:?package=PureGate.Socket&version=1.0.3
                    
Install as a Cake Tool

PureGate.Socket

NuGet CI License: MIT

A fast, dependency-free messaging layer for long-lived TCP connections between .NET services.

PureGate.Socket gives two processes one connection that both of them can use: either side can call the other, send it events, or stream data to it. Every message is framed, multiplexed and flow-controlled, so many concurrent calls share one socket without blocking each other or overrunning the receiver. There's no HTTP, no code generation and no runtime dependency beyond .NET itself.

dotnet add package PureGate.Socket

Features

Everything two .NET services need to talk over one long-lived connection, in a single package with no dependencies.

Messaging

  • Request/response, typed or raw bytes
  • Fire-and-forget events, handled in order per connection
  • Pull-based binary streams of any size
  • Batches: many requests or events in one round trip
  • Both ends are peers: a server calls its clients as easily as they call it
  • Large messages chunked automatically, with optional Brotli compression

Under load

  • Credit-based flow control: a busy receiver slows its senders instead of being overrun
  • A window per stream that grows to fit the link, so a slow reader holds back only its own stream
  • A memory budget per connection: a message that doesn't fit is refused, not buffered
  • Bounded handler concurrency and queues

When things fail

  • Deadlines travel with the request, and cancelling a call cancels the remote handler
  • Half-open connections detected in seconds
  • A client that reconnects on its own, by address or host name
  • Idempotent retries that the server runs once

Security

  • Mutual HMAC authentication with a secret per pair of nodes
  • TLS and mutual TLS, with node IDs bound to certificates
  • Authorization per request, stream and event
  • Limits on handshakes and connections before a peer authenticates

Operations

  • OpenTelemetry-ready metrics and observer hooks
  • A peer registry to reach a node by ID or broadcast to all
  • Pluggable serialization: JSON (source-generated for AOT), raw bytes, or your own codec, such as MessagePack
  • Fuzz, chaos and soak tested

Speed

  • 77% of a bare socket's call rate, and over half a million calls a second on one shared connection
  • Faster than gRPC, SignalR and StreamJsonRpc in every column of the benchmark, with the fewest allocations

Compared with the alternatives

PureGate.Socket gRPC SignalR StreamJsonRpc
Either side can call the other ✓ Through streams only ✓ ✓
Hosting Any process, plain TCP ASP.NET Core, HTTP/2 ASP.NET Core, WebSockets Any stream
Contract Plain C# types .proto files and code generation Hub methods Interfaces or method names
Runtime dependencies None gRPC, Protobuf, ASP.NET Core ASP.NET Core Several packages

Performance

Echoing a 1 KB payload with libraries' recommended fast setup:

Library Sequential calls/s p50 p99 64 callers calls/s Alloc per call
Raw socket (ceiling) 47,558 20 µs 36 µs n/a 128 B
PureGate.Socket 36,668 26 µs 39 µs 548,710 3,253 B
gRPC (HTTP/2) 6,063 161 µs 236 µs 120,166 10,345 B
SignalR (MessagePack) 24,335 37 µs 65 µs 329,210 4,422 B
StreamJsonRpc (MessagePack) 27,067 32 µs 50 µs 279,519 5,162 B
  • Lowest latency: 77% of a bare socket's call rate, with the best p50 and p99 of the four libraries. That's 1.35× StreamJsonRpc, 1.5× SignalR and 6× gRPC.
  • Highest throughput on one connection: with 64 callers sharing a single connection, 1.7× SignalR, 2× StreamJsonRpc and 4.6× gRPC.
  • Least memory: the fewest bytes allocated per call, counting both client and server.

Measured on Windows, 20 cores, .NET 10, over loopback with client and server in one process. "Sequential" makes one call at a time; "64 callers" share one connection. Numbers vary by machine; the benchmark and its methodology are in the repository, so you can run it yourself.

Quick start

A server answers requests with an ISocketPeerHandler. Only HandleRequestAsync is required. Each request carries a service and an operation name to route on.

using System.Net;
using PureGate.Socket.Fabric;
using PureGate.Socket.Handling;
using PureGate.Socket.Protocol;
using PureGate.Socket.Transport;

// Server
await using var listener = new SocketListener(new SocketFabricOptions { NodeId = "greeter" });
await listener.StartAsync(new IPEndPoint(IPAddress.Any, 5000), new GreeterHandler());

// Client: connects on first use and reconnects when the connection drops
await using var client = new SocketClient("localhost", 5000, handler: null, new SocketFabricOptions { NodeId = "web-1" });
var reply = await client.RequestAsync<Greeting, Reply>("greeter", "hello", new Greeting("Ada"));
Console.WriteLine(reply.Message); // Hello, Ada!

sealed record Greeting(string Name);
sealed record Reply(string Message);

sealed class GreeterHandler : ISocketPeerHandler
{
  public ValueTask<SocketResponse> HandleRequestAsync(SocketPeer peer, SocketRequest request, CancellationToken cancellationToken)
  {
    var greeting = request.ReadPayload<Greeting>(peer.Codec);
    return ValueTask.FromResult(SocketResponse.Success(peer.Codec, new Reply($"Hello, {greeting.Name}!")));
  }
}

Messages are JSON by default. Use RawSocketMessageCodec to send bytes as they are, or implement your own ISocketMessageCodec.

Guide

Events

Events are one-way. By default, events from one connection are handled in the order they were sent (EventOrdering).

await client.SendEventAsync("orders", "created", new OrderCreated(42));
// In the handler
public ValueTask HandleEventAsync(SocketPeer peer, SocketEvent socketEvent, CancellationToken cancellationToken)
{
  var created = socketEvent.ReadPayload<OrderCreated>(peer.Codec);
  return ValueTask.CompletedTask;
}

Streams

A stream request returns a Stream that the caller reads at its own pace. The producer only sends what the reader has room for.

// In the handler
public ValueTask<SocketStreamResponse> HandleStreamAsync(SocketPeer peer, SocketStreamRequest request, CancellationToken cancellationToken)
{
  var name = Path.GetFileName(request.ReadPayload<string>(peer.Codec));
  return ValueTask.FromResult(SocketStreamResponse.Success(File.OpenRead(Path.Combine("exports", name))));
}
await using var download = await client.OpenStreamAsync("files", "export", "orders.csv");
await using var file = File.Create("orders.csv");
await download.CopyToAsync(file);

Batches

Send many requests in one round trip, each with its own result.

var results = await peer.RequestBatchAsync(
[
  new SocketBatchItem(1, "orders", "get", peer.Codec.Serialize(new OrderId(1))),
  new SocketBatchItem(2, "orders", "get", peer.Codec.Serialize(new OrderId(2)))
], TimeSpan.FromSeconds(5));

Calling clients from the server

Every connection is a SocketPeer, and both of its ends can send. A SocketPeerRegistry is an observer that keeps track of the connected peers, so a server can reach a node by its ID or broadcast to all of them.

var registry = new SocketPeerRegistry();
await using var hub = new SocketListener(new SocketFabricOptions { NodeId = "hub", Observer = registry });
// ...
var status = await registry.RequestAsync<StatusQuery, Status>("web-1", "health", "status", new StatusQuery());

Timeouts, retries and idempotency

Every call has a timeout (RequestTimeout, 10 seconds by default), and the deadline travels with the request: the remote handler's cancellation token fires when it passes. Cancelling a call cancels the remote handler too.

SocketClient can retry. Mark a call idempotent and every attempt carries the same idempotency key, so the server runs it once and answers repeats from its cache:

var id = await client.RequestAsync<CreateOrder, OrderId>(
    "orders", "create", order,
    new SocketClientRequestOptions { Idempotent = true, MaxAttempts = 3 });

When a call times out, the client first checks whether the connection is still alive. A half-open connection is closed, so the retry goes out on a new one instead of timing out again.

Security

Authentication runs in the handshake, and nothing else is accepted before it succeeds.

var options = new SocketFabricOptions
{
  NodeId = "api-1",

  // Each pair of nodes shares its own secret, so a node can only authenticate as itself.
  Authenticator = new HmacSocketAuthenticator(remoteNodeId => secrets.For(remoteNodeId)),

  // TLS, with each node's ID bound to its certificate.
  StreamFilter = TlsSocketStreamFilter.Create(certificate, "api-1", requireClientCertificate: true, bindNodeIdToCertificate: true),

  // Per-request, per-stream and per-event checks.
  Authorizer = new MyAuthorizer()
};

new HmacSocketAuthenticator("shared-secret") uses one secret for the whole cluster. Before a peer authenticates, handshake frames are limited to 16 KB, and a listener caps concurrent handshakes (MaxPendingHandshakes) and connections (MaxConnections).

Metrics

Everything is published under the meter PureGate.Socket:

builder.Services.AddOpenTelemetry().WithMetrics(metrics => metrics.AddMeter(SocketMetrics.MeterName));

You can also watch them with dotnet-counters monitor --counters PureGate.Socket. Nothing is measured until a listener subscribes. For hooks into connection and request events, implement ISocketObserver.

Serialization and AOT

JsonSocketMessageCodec uses System.Text.Json. For native AOT and trimming, give it a source-generated context:

[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
[JsonSerializable(typeof(Greeting))]
[JsonSerializable(typeof(Reply))]
partial class AppJson : JsonSerializerContext;

var options = new SocketFabricOptions { Codec = new JsonSocketMessageCodec(AppJson.Default) };

For the fastest typed calls, use a binary format such as MessagePack. Add the MessagePack package to your project and copy this codec; PureGate.Socket itself stays dependency-free:

using System.Buffers;
using MessagePack;
using PureGate.Socket;

public sealed class MessagePackSocketMessageCodec : ISocketMessageCodec
{
    public static readonly MessagePackSocketMessageCodec Shared = new();

    public void Serialize<T>(T value, IBufferWriter<byte> output) => MessagePackSerializer.Serialize(output, value);

    public T Deserialize<T>(ReadOnlySequence<byte> payload) => MessagePackSerializer.Deserialize<T>(payload);
}

var options = new SocketFabricOptions { Codec = MessagePackSocketMessageCodec.Shared };

Limits and defaults

Every limit is an option on SocketFabricOptions.

Option Default What it bounds
RequestTimeout 10 s Each call, unless the call passes its own timeout
MaxFrameSizeBytes 1 MB One frame; larger messages are split into chunks automatically
MaxDecompressedPayloadSizeBytes 128 MB One message, after reassembly and decompression
MaxConnectionMemoryBytes 256 MB Everything a connection holds for its remote
MaxInFlightRequests 1,024 This side's outstanding calls and streams on a connection
MaxConcurrentHandlers 100 Handlers running at once on a connection
MaxQueuedHandlers 4,096 Messages waiting for a handler, which is also how many the remote may send ahead
StreamWindowBytes 1 MB Unread data per stream, growing up to MaxStreamWindowBytes (64 MB)
HeartbeatInterval / IdleTimeout 15 s / 45 s Detection of dead connections
MaxPendingHandshakes / MaxConnections 256 / unlimited What a listener admits
SocketBufferBytes 1 MB on Windows, system default elsewhere Kernel send and receive buffers per connection

Documentation

  • Library guide: flow control, the memory budget, serialization and the full list of metrics.
  • Wire protocol: the frame format, the handshake, and flow control on the wire.
  • Competitor benchmark: PureGate.Socket against gRPC, SignalR and StreamJsonRpc, with results.
  • Roadmap.
  • The tests are working examples of every feature.

Building and testing

dotnet build PureGate.Socket.slnx
dotnet test src/PureGate.Socket.Test

The suite includes seeded fuzz tests of the decoder and chaos tests through a proxy that fragments, delays and resets connections. A soak test, excluded by default, runs mixed chaotic traffic and then checks that every connection's state and every pooled buffer drains:

dotnet test src/PureGate.Socket.Test -c Release --filter Category=Soak

It runs for PUREGATE_SOAK_SECONDS (20 by default). The manual soak workflow on GitHub runs it for an hour.

When contributing: the build treats warnings as errors, and CI requires 85% line coverage.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 46 11/12/2026
1.0.9 68 11/9/2026
1.0.8 71 11/8/2026
1.0.7 66 11/7/2026
1.0.6 73 11/7/2026
1.0.5 67 11/7/2026
1.0.4 72 11/7/2026
1.0.3 73 11/6/2026
1.0.2 66 11/6/2026
1.0.1 62 11/6/2026
1.0.0 69 11/6/2026