Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
11c96f8
feat(networking): introduce message-oriented networking transports
ReubenBond Aug 22, 2026
c12c096
fix(networking): address transport review feedback
ReubenBond Aug 24, 2026
117370d
fix(networking): dispose failed connection transports
ReubenBond Aug 24, 2026
9ae1a6d
perf(networking): optimize transport message handling
ReubenBond Aug 25, 2026
5b76894
perf(networking): batch message transport writes
ReubenBond Aug 26, 2026
7c5cdac
perf(benchmarks): add TLS transport comparison
ReubenBond Aug 26, 2026
f0921f1
perf(benchmarks): expand transport scenarios
ReubenBond Aug 26, 2026
3ae9b4e
perf(networking): gather larger message batches
ReubenBond Aug 26, 2026
7274bb9
perf(networking): pipeline large message sends
ReubenBond Aug 26, 2026
6d72240
fix(networking): reset socket send buffers
ReubenBond Aug 26, 2026
6a8d587
fix(networking): tighten codec and frame contracts
ReubenBond Aug 26, 2026
afd6e74
perf(networking): reduce transport scheduling overhead
ReubenBond Aug 26, 2026
8337cf1
refactor(networking): simplify message transports
ReubenBond Aug 26, 2026
82fb22a
fix(test): propagate cancellation after rebase
ReubenBond Aug 28, 2026
d67a2f5
fix(networking): address remaining transport review feedback
ReubenBond Aug 28, 2026
3d8d9f2
perf(networking): cache socket completion errors
ReubenBond Aug 28, 2026
ed63c9b
fix(networking): resolve transport rebase conflicts
ReubenBond Sep 2, 2026
fd3ef95
fix(networking): address listener review feedback
ReubenBond Sep 2, 2026
593db79
fix(networking): make listener disposal idempotent
ReubenBond Sep 2, 2026
3184044
fix(networking): preserve listener and log contracts
ReubenBond Sep 2, 2026
2c4edca
fix(testing): guarantee Unix socket cleanup
ReubenBond Sep 2, 2026
a708e4a
fix(networking): preserve transport guarantees after rebase
ReubenBond Sep 3, 2026
9b74ca7
fix(networking): align transports with analyzer baseline
ReubenBond Sep 3, 2026
a5ee346
fix(networking): complete analyzer and TLS contracts
ReubenBond Sep 3, 2026
0195dd3
fix(networking): preserve transport lifecycle guarantees
ReubenBond Sep 3, 2026
d3fbc38
fix(testing): remove stale serializer conflict markers
ReubenBond Sep 4, 2026
be754e9
fix(networking): apply socket options and formatting
ReubenBond Sep 4, 2026
094c521
fix(networking): preserve disposable ownership after rebase
ReubenBond Sep 4, 2026
0030e1d
fix(networking): finalize pooled transport contracts
ReubenBond Sep 4, 2026
c6de2f6
fix(testing): preserve transport defaults
ReubenBond Sep 4, 2026
a6bbfa5
fix(networking): preserve message handler shutdown lifetime
ReubenBond Sep 4, 2026
646d7f5
fix(networking): quiesce send workers before teardown
ReubenBond Sep 4, 2026
540a55d
docs(networking): clarify transport request contracts
ReubenBond Sep 4, 2026
d4fbea0
test(networking): cover TLS client authentication options
ReubenBond Sep 4, 2026
8978a04
fix(networking): preserve upstream guarantees after rebase
ReubenBond Oct 3, 2026
c98d097
fix(hosting): preserve registered client providers
ReubenBond Oct 3, 2026
12f9138
refactor(networking): remove receiver-cache optimization
ReubenBond Oct 3, 2026
b48d0ef
fix(networking): address transport review feedback
ReubenBond Oct 3, 2026
7b2a3d3
fix(networking): restore public TLS certificate loading
ReubenBond Oct 3, 2026
322ee9b
fix(networking): preserve nullable TLS callback adapters
ReubenBond Oct 3, 2026
a89c496
fix(networking): restore TCP keep-alive defaults
ReubenBond Oct 4, 2026
befef2a
fix(networking): tolerate unsupported keep-alive tuning
ReubenBond Oct 4, 2026
ea56293
fix(networking): preserve transport completion and buffer ownership
ReubenBond Oct 4, 2026
df0f50c
fix(networking): complete status and connector disposal contracts
ReubenBond Oct 4, 2026
b7fb89a
fix(networking): preserve shutdown cache invalidation
ReubenBond Oct 4, 2026
a69cd83
fix(networking): address API and teardown review feedback
ReubenBond Oct 5, 2026
e6c8a50
fix(networking): honor stopped listener accept contracts
ReubenBond Oct 5, 2026
5408054
docs(networking): describe transport failure exceptions
ReubenBond Oct 5, 2026
482caef
refactor(networking): rely on project nullable settings
ReubenBond Oct 5, 2026
57a8ee8
fix(networking): restore nullable contracts and regenerate APIs
ReubenBond Oct 5, 2026
b7cdf20
fix(networking): honor TLS revocation and rejection cleanup
ReubenBond Oct 5, 2026
1853318
fix(networking): enforce publication and client certificate requirements
ReubenBond Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
<PackageVersion Include="MessagePack" Version="3.1.7" />
<PackageVersion Include="Microsoft.NET.StringTools" Version="17.11.4" />
<PackageVersion Include="Microsoft.Accordant" Version="0.1.6" />
<PackageVersion Include="Microsoft.AspNetCore.Connections.Abstractions" Version="8.0.24" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.57.0" />
<PackageVersion Include="Microsoft.Build" Version="17.10.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.9.0" />
Expand Down Expand Up @@ -147,7 +146,6 @@
<PackageVersion Update="Aspire.StackExchange.Redis" Version="13.2.4" />
<PackageVersion Update="BenchmarkDotNet" Version="0.15.6" />
<PackageVersion Update="FSharp.Core" Version="10.0.103" />
<PackageVersion Update="Microsoft.AspNetCore.Connections.Abstractions" Version="10.0.3" />
<PackageVersion Update="Microsoft.CodeAnalysis.Common" Version="5.0.0" />
<PackageVersion Update="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
<PackageVersion Update="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
Expand Down
1 change: 0 additions & 1 deletion Orleans.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@
<Folder Name="/src/Extensions/">
<Project Path="src/Orleans.Clustering.Consul/Orleans.Clustering.Consul.csproj" />
<Project Path="src/Orleans.Clustering.ZooKeeper/Orleans.Clustering.ZooKeeper.csproj" />
<Project Path="src/Orleans.Connections.Security/Orleans.Connections.Security.csproj" />
<Project Path="src/Orleans.Hosting.Kubernetes/Orleans.Hosting.Kubernetes.csproj" />
</Folder>
<Folder Name="/src/Extensions/AdoNet/">
Expand Down
50 changes: 25 additions & 25 deletions docs/site/src/content/docs/host/connection-middleware.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,55 @@
---
title: Connection middleware
description: Plug custom logic, such as authentication or custom framing, into Orleans silo-to-silo and client-to-silo connections.
ms.date: 08/11/2026
title: Message transport middleware
description: Decorate Orleans message transport connectors and listeners with cross-cutting connection behavior.
ms.date: 08/21/2026
ms.topic: how-to
---

# Connection middleware
# Message transport 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. <xref:Orleans.Runtime.Messaging.IConnectionMiddleware> lets you insert custom logic, such as authentication, custom framing, or connection-level diagnostics, into that pipeline without reimplementing connection setup.
Orleans establishes silo-to-silo and client-to-gateway connections through message transport connectors and listeners. Transport middleware decorates those components before they create or accept a connection.

Orleans itself uses this abstraction for TLS. See [Secure Orleans connections with TLS](transport-layer-security.md) for the built-in TLS middleware.
<xref:Orleans.Connections.Transport.IMessageTransportConnectorMiddleware> applies to outbound connectors. <xref:Orleans.Connections.Transport.IMessageTransportListenerMiddleware> applies to inbound listeners. Orleans applies every registered middleware instance in dependency-injection registration order.

## 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 <xref:Microsoft.AspNetCore.Connections.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.
Orleans uses transport middleware to apply TLS. See [Secure Orleans connections with TLS](transport-layer-security.md) for the built-in security configuration.

## Register middleware

Use <xref:Orleans.ConnectionMiddlewareExtensions.UseMiddleware*> on an <xref:Microsoft.AspNetCore.Connections.IConnectionBuilder> to add middleware to a connection pipeline:
Register connector and listener middleware as singleton services:

:::code language="csharp" source="./snippets/connection-middleware/ConnectionMiddlewareExamples.cs" id="RegisterMiddleware":::

Connection pipelines are configured through <xref:Orleans.Configuration.SiloConnectionOptions> (silo) and <xref:Orleans.Configuration.ClientConnectionOptions> (client). A silo has three distinct pipelines:
The middleware instances can be invoked for multiple connectors or listeners, so keep per-connection state in the returned decorator or the resulting <xref:Orleans.Connections.Transport.MessageTransport>.

## Decorate outbound connectors

:::code language="csharp" source="./snippets/connection-middleware/ConnectionMiddlewareExamples.cs" id="SiloPipelines":::
A connector middleware receives the next <xref:Orleans.Connections.Transport.MessageTransportConnector> and returns the connector Orleans should use. The decorator can observe connection attempts, select another transport, or wrap the returned message transport:

An Orleans client has a single outbound pipeline, configured with <xref:Orleans.Configuration.ClientConnectionOptions>:
:::code language="csharp" source="./snippets/connection-middleware/ConnectionMiddlewareExamples.cs" id="ConnectorDecorator":::

:::code language="csharp" source="./snippets/connection-middleware/ConnectionMiddlewareExamples.cs" id="ClientPipeline":::
Delegate <xref:Orleans.Connections.Transport.MessageTransportConnector.Features>, `IsValid`, and disposal unless the middleware deliberately changes those guarantees.

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.
Orleans owns the connector decorators returned by middleware. During host disposal, the connection factory closes connection-establishment admission, awaits admitted attempts, and disposes the decorated connector chain. Each decorator disposes its own resources and delegates disposal to its inner connector. The service provider owns the registered transport connector and disposes it separately.

> [!NOTE]
> Silo-to-silo and client-to-silo connections perform an Orleans-internal handshake after the connection pipeline runs. Middleware that terminates or replaces the transport (such as TLS) must leave a working `ConnectionContext` in place before calling `next`, because Orleans handshake and message framing execute afterward.
## Decorate inbound listeners

## Read and write framed data
A listener middleware receives the next <xref:Orleans.Connections.Transport.MessageTransportListener> and returns the listener Orleans should bind and accept from:

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 <xref:Orleans.Runtime.Messaging.ConnectionFrameHelper> for structured, length-prefixed frames. `ConnectionFrameHelper` is optional; it exists to save middleware authors from re-implementing length-prefixed framing.
:::code language="csharp" source="./snippets/connection-middleware/ConnectionMiddlewareExamples.cs" id="ListenerDecorator":::

The wire format per frame is `[4-byte little-endian length][1-byte frame type][payload]`, where the length equals `1 + payload.Length`.
Preserve the listener name so Orleans can select the silo and gateway listeners by role. Return `null` from `AcceptAsync` when the inner listener has stopped, matching the listener contract.

:::code language="csharp" source="./snippets/connection-middleware/ConnectionMiddlewareExamples.cs" id="ServerMiddleware":::
## Migrate byte-stream middleware

`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.
Customizations previously registered through `ClientConnectionOptions`, `SiloConnectionOptions`, or `IConnectionMiddleware` migrate to the connector and listener decorators shown above. Register outbound behavior as connector middleware and inbound behavior as listener middleware. Listener decorators can use the listener name to configure the `silo` and `gateway` roles independently.

`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.
The message transport request contracts own framing, completion, and buffer lifetimes. Decorators preserve those contracts while applying per-connection behavior.

## See also

- <xref:Orleans.Runtime.Messaging.IConnectionMiddleware>
- <xref:Orleans.Runtime.Messaging.ConnectionFrameHelper>
- <xref:Orleans.Connections.Transport.MessageTransport>
- <xref:Orleans.Connections.Transport.MessageTransportConnector>
- <xref:Orleans.Connections.Transport.MessageTransportListener>
- [Secure Orleans connections with TLS](transport-layer-security.md)
- [Server configuration](configuration-guide/server-configuration.md)
- [Client configuration](configuration-guide/client-configuration.md)
Original file line number Diff line number Diff line change
@@ -1,128 +1,84 @@
using System.Text;
using Microsoft.AspNetCore.Connections;
using System.Net;
using Microsoft.Extensions.DependencyInjection;
using Orleans;
using Orleans.Configuration;
using Orleans.Hosting;
using Orleans.Runtime.Messaging;
using Microsoft.Extensions.Logging;
using Orleans.Connections.Transport;

namespace ConnectionMiddlewareSnippets;

internal static class ConnectionMiddlewareExamples
{
// <RegisterMiddleware>
public static void RegisterFromDependencyInjection(
IServiceCollection services,
IConnectionBuilder connectionBuilder)
public static void RegisterTransportMiddleware(IServiceCollection services)
{
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());
services.AddSingleton<IMessageTransportConnectorMiddleware, LoggingConnectorMiddleware>();
services.AddSingleton<IMessageTransportListenerMiddleware, LoggingListenerMiddleware>();
}
// </RegisterMiddleware>
}

// <SiloPipelines>
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>();
});
internal sealed class LoggingConnectorMiddleware(ILoggerFactory loggerFactory)
: IMessageTransportConnectorMiddleware
{
public MessageTransportConnector Apply(MessageTransportConnector transport) =>
new LoggingConnector(transport, loggerFactory.CreateLogger<LoggingConnector>());
}

// Connections this silo accepts from other silos.
options.ConfigureSiloInboundConnection(connectionBuilder =>
{
connectionBuilder.UseMiddleware<MyServerSideMiddleware>();
});
internal sealed class LoggingListenerMiddleware(ILoggerFactory loggerFactory)
: IMessageTransportListenerMiddleware
{
public MessageTransportListener Apply(MessageTransportListener listener) =>
new LoggingListener(listener, loggerFactory.CreateLogger<LoggingListener>());
}

// Connections this silo accepts from Orleans clients through the gateway.
options.ConfigureGatewayInboundConnection(connectionBuilder =>
{
connectionBuilder.UseMiddleware<MyServerSideMiddleware>();
});
});
}
// </SiloPipelines>
// <ConnectorDecorator>
internal sealed class LoggingConnector(
MessageTransportConnector inner,
ILogger<LoggingConnector> logger) : MessageTransportConnector
{
public override IFeatureCollection Features => inner.Features;
public override bool IsValid => inner.IsValid;

// <ClientPipeline>
public static void ConfigureClient(IClientBuilder clientBuilder)
public override async ValueTask<MessageTransport> CreateAsync(
EndPoint endpoint,
CancellationToken cancellationToken = default)
{
clientBuilder.Services.AddSingleton<MyClientSideMiddleware>();

clientBuilder.Configure<ClientConnectionOptions>(options =>
{
options.ConfigureConnection(connectionBuilder =>
{
connectionBuilder.UseMiddleware<MyClientSideMiddleware>();
});
});
logger.LogInformation("Connecting Orleans transport to {Endpoint}", endpoint);
return await inner.CreateAsync(endpoint, cancellationToken);
}
// </ClientPipeline>

public override ValueTask DisposeAsync() => inner.DisposeAsync();
}
// </ConnectorDecorator>

internal sealed class MyClientSideMiddleware : IConnectionMiddleware
// <ListenerDecorator>
internal sealed class LoggingListener(
MessageTransportListener inner,
ILogger<LoggingListener> logger) : MessageTransportListener
{
public async Task OnConnectionAsync(
ConnectionContext context,
ConnectionDelegate next)
{
var requestPayload = Encoding.UTF8.GetBytes("hello");
await ConnectionFrameHelper.WriteFrameAsync(
context, frameType: 0x01, requestPayload, context.ConnectionClosed);
public override bool IsValid => inner.IsValid;
public override string ListenerName => inner.ListenerName;
public override IFeatureCollection Features => inner.Features;

var (frameType, payload) = await ConnectionFrameHelper.ReadFrameAsync(
context, context.ConnectionClosed);
public override ValueTask BindAsync(CancellationToken cancellationToken = default) =>
inner.BindAsync(cancellationToken);

if (frameType != 0x02
|| !string.Equals(
Encoding.UTF8.GetString(payload),
"ok",
StringComparison.Ordinal))
public override async ValueTask<MessageTransport?> AcceptAsync(
CancellationToken cancellationToken = default)
{
var transport = await inner.AcceptAsync(cancellationToken);
if (transport is not null)
{
throw new InvalidOperationException("Unexpected handshake response.");
logger.LogInformation(
"Accepted Orleans transport on listener {ListenerName}",
ListenerName);
}

await next(context);
return transport;
}
}

// <ServerMiddleware>
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);
public override ValueTask UnbindAsync(CancellationToken cancellationToken = default) =>
inner.UnbindAsync(cancellationToken);

// Continue the pipeline; Orleans's own handshake and framing run after this.
await next(context);
}
public override ValueTask DisposeAsync() => inner.DisposeAsync();
}
// </ServerMiddleware>
// </ListenerDecorator>
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RepositoryRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\..\..\..\..\..\..\..\..\'))</RepositoryRoot>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Client" Version="10.2.2" />
<PackageReference Include="Microsoft.Orleans.Connections.Security" Version="10.2.2" />
<ProjectReference Include="$(RepositoryRoot)src\Orleans.Client\Orleans.Client.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Hosting;
using Orleans.Connections.Security;
using Orleans.Connections.Transport.Security;
using Orleans.Hosting;

Console.WriteLine("TLS configuration examples");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Hosting;
using Orleans.Connections.Security;
using Orleans.Connections.Transport.Security;
using Orleans.Hosting;

Console.WriteLine("TLS configuration examples");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RepositoryRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\..\..\..\..\..\..\..\..\'))</RepositoryRoot>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Connections.Security" Version="10.2.2" />
<PackageReference Include="Microsoft.Orleans.Server" Version="10.2.2" />
<ProjectReference Include="$(RepositoryRoot)src\Orleans.Server\Orleans.Server.csproj" />
</ItemGroup>

</Project>
Loading
Loading