Skip to content

Connection middleware

Orleans silo-to-silo and client-to-gateway connections are built from a chain of middleware, similar to an ASP.NET Core request pipeline. IConnectionMiddleware lets you insert custom logic, such as authentication, custom framing, or connection-level diagnostics, into that pipeline without reimplementing connection setup.

Orleans itself uses this abstraction for TLS. See Secure Orleans connections with TLS for the built-in TLS middleware.

The interface defines OnConnectionAsync(ConnectionContext, ConnectionDelegate). A middleware instance can be invoked concurrently for multiple connections, so store per-connection state in local variables or on the ConnectionContext, not on the middleware instance. Implementations should call next(context) to continue the pipeline after performing their work; not calling next terminates the connection.

Use UseMiddleware on an IConnectionBuilder to add middleware to a connection pipeline:

public static void RegisterFromDependencyInjection(
IServiceCollection services,
IConnectionBuilder connectionBuilder)
{
services.AddSingleton<MyClientSideMiddleware>();
connectionBuilder.UseMiddleware<MyClientSideMiddleware>();
}
public static void RegisterInstance(IConnectionBuilder connectionBuilder)
{
// The caller owns this shared instance and is responsible for disposing it.
connectionBuilder.UseMiddleware(new MyClientSideMiddleware());
}

Connection pipelines are configured through SiloConnectionOptions (silo) and ClientConnectionOptions (client). A silo has three distinct pipelines:

public static void ConfigureSilo(ISiloBuilder siloBuilder)
{
siloBuilder.Services.AddSingleton<MyClientSideMiddleware>();
siloBuilder.Services.AddSingleton<MyServerSideMiddleware>();
siloBuilder.Configure<SiloConnectionOptions>(options =>
{
// Connections this silo makes to other silos.
options.ConfigureSiloOutboundConnection(connectionBuilder =>
{
connectionBuilder.UseMiddleware<MyClientSideMiddleware>();
});
// Connections this silo accepts from other silos.
options.ConfigureSiloInboundConnection(connectionBuilder =>
{
connectionBuilder.UseMiddleware<MyServerSideMiddleware>();
});
// Connections this silo accepts from Orleans clients through the gateway.
options.ConfigureGatewayInboundConnection(connectionBuilder =>
{
connectionBuilder.UseMiddleware<MyServerSideMiddleware>();
});
});
}

An Orleans client has a single outbound pipeline, configured with ClientConnectionOptions:

public static void ConfigureClient(IClientBuilder clientBuilder)
{
clientBuilder.Services.AddSingleton<MyClientSideMiddleware>();
clientBuilder.Configure<ClientConnectionOptions>(options =>
{
options.ConfigureConnection(connectionBuilder =>
{
connectionBuilder.UseMiddleware<MyClientSideMiddleware>();
});
});
}

Middleware added first runs first, wrapping every middleware added after it, matching the order UseMiddleware is called. Register the middleware type itself (for example MyClientSideMiddleware, MyServerSideMiddleware) as a singleton service when using the generic UseMiddleware&lt;T>() overload.

Custom middleware that exchanges its own protocol data before calling next (for example, a handshake) can read and write directly from context.Transport.Input/Output (PipeReader/PipeWriter), or use ConnectionFrameHelper for structured, length-prefixed frames. ConnectionFrameHelper is optional; it exists to save middleware authors from re-implementing length-prefixed framing.

The wire format per frame is [4-byte little-endian length][1-byte frame type][payload], where the length equals 1 + payload.Length.

internal sealed class MyServerSideMiddleware : IConnectionMiddleware
{
public async Task OnConnectionAsync(
ConnectionContext context,
ConnectionDelegate next)
{
// Read one frame of the custom handshake protocol.
var (frameType, payload) = await ConnectionFrameHelper.ReadFrameAsync(
context, context.ConnectionClosed);
if (frameType != 0x01
|| !string.Equals(
Encoding.UTF8.GetString(payload),
"hello",
StringComparison.Ordinal))
{
throw new InvalidOperationException("Unexpected handshake request.");
}
var responsePayload = Encoding.UTF8.GetBytes("ok");
await ConnectionFrameHelper.WriteFrameAsync(
context, frameType: 0x02, responsePayload, context.ConnectionClosed);
// Continue the pipeline; Orleans's own handshake and framing run after this.
await next(context);
}
}

ConnectionFrameHelper also provides WriteLengthPrefixedString/ReadLengthPrefixedString helpers for encoding UTF-8 strings inside a frame payload, and a zero-copy WriteFrameAsync overload that writes the payload directly into the transport pipe buffer via an Action&lt;IBufferWriter&lt;byte>> delegate, avoiding an intermediate byte[] allocation.

ReadFrameAsync throws InvalidOperationException if the connection is closed mid-frame or if the declared frame length exceeds maxFrameLength (ConnectionFrameHelper.DefaultMaxFrameLength, 1 MB, by default). Pass a smaller maxFrameLength if your protocol’s frames are bounded more tightly, to fail fast on malformed or hostile input.