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 middleware interface
Section titled “The middleware interface”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.
Register middleware
Section titled “Register middleware”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<T>() overload.
Read and write framed data
Section titled “Read and write framed data”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<IBufferWriter<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.
