High-performance .NET 10 async networking. Bidirectional server-client communication. Source-generated, AOT-friendly. MemoryPack serialization.
<PackageReference Include="NexNet" Version="0.15.0" />
<PackageReference Include="NexNet.Generator" Version="0.15.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- Optional: NexNet.Quic, NexNet.Asp -->// Shared interfaces
public interface IClientNexus { ValueTask ReceiveMessage(string msg); }
public interface IServerNexus { ValueTask SendMessage(string msg); }
// Client implements IClientNexus, calls IServerNexus via Proxy
[Nexus<IClientNexus, IServerNexus>(NexusType = NexusType.Client)]
public partial class ClientNexus {
public ValueTask ReceiveMessage(string msg) => ValueTask.CompletedTask;
}
// Server implements IServerNexus, calls IClientNexus via Proxy
[Nexus<IServerNexus, IClientNexus>(NexusType = NexusType.Server)]
public partial class ServerNexus {
public ValueTask SendMessage(string msg) => ValueTask.CompletedTask;
}
// Usage
var server = ServerNexus.CreateServer(serverConfig, () => new ServerNexus());
await server.StartAsync();
var client = ClientNexus.CreateClient(clientConfig, new ClientNexus());
await client.ConnectAsync();
await client.Proxy.SendMessage("Hello");Nexus classes must be partial, not abstract/nested/generic. One instance per connection. No constructor work (pooled via ContextProvider).
| Return | Behavior | Allowed Params |
|---|---|---|
void |
Fire-and-forget | args only |
ValueTask |
Await completion | args + CT, OR args + pipes/channels |
ValueTask<T> |
Await + return | args + CT only |
CancellationToken must be last. Max serialized args: 65,535 bytes (use pipes for larger).
// Both
protected override ValueTask OnConnected(bool isReconnected) => default;
protected override ValueTask OnDisconnected(Exception? ex) => default;
// Client only
protected override ValueTask OnReconnecting() => default;
// Server only (null = reject auth)
protected override ValueTask<IIdentity?> OnAuthenticate(ReadOnlyMemory<byte>? token) => ...;
// Server only (after auth, before OnConnected)
protected override ValueTask OnNexusInitialize() => default;| Scenario | Server Config | Client Config |
|---|---|---|
| Unix IPC | UdsServerConfig |
UdsClientConfig |
| TCP | TcpServerConfig |
TcpClientConfig |
| TLS/TCP | TcpTlsServerConfig |
TcpTlsClientConfig |
| QUIC | QuicServerConfig |
QuicClientConfig |
| WebSocket | ASP.NET server | WebSocketClientConfig |
| HttpSocket | ASP.NET server | HttpSocketClientConfig |
// TCP
new TcpServerConfig { EndPoint = new IPEndPoint(IPAddress.Any, 1234) };
new TcpClientConfig { EndPoint = new IPEndPoint(IPAddress.Loopback, 1234) };
// UDS
new UdsServerConfig { EndPoint = new UnixDomainSocketEndPoint("/tmp/app.sock") };
// TLS - set SslServerAuthenticationOptions / SslClientAuthenticationOptions
new TcpTlsServerConfig {
EndPoint = new IPEndPoint(IPAddress.Any, 1234),
SslServerAuthenticationOptions = new() {
ServerCertificate = X509CertificateLoader.LoadPkcs12FromFile("server.pfx", "pass"),
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
}
};
// QUIC (requires NexNet.Quic; libmsquic on Linux)
new QuicServerConfig {
EndPoint = new IPEndPoint(IPAddress.Any, 1234),
SslServerAuthenticationOptions = new() { ... }
};
// WebSocket/HttpSocket clients
new WebSocketClientConfig { Url = new Uri("ws://localhost:5000/nexus") };
new HttpSocketClientConfig { Url = new Uri("http://localhost:5000/nexus") };
// Both support: AuthenticationHeader = new AuthenticationHeaderValue("Bearer", "token")| Property | Default | Description |
|---|---|---|
Logger |
null | INexusLogger instance |
MaxConcurrentConnectionInvocations |
2 | 1-1000 |
DisconnectDelay |
200ms | 0-10000ms |
Timeout |
30000ms | 50-300000ms, idle timeout |
HandshakeTimeout |
15000ms | 50-60000ms |
NexusPipeFlushChunkSize |
8KB | 1KB-1MB |
NexusPipeHighWaterMark |
192KB | Pause writer threshold |
NexusPipeLowWaterMark |
16KB | Resume threshold |
NexusPipeHighWaterCutoff |
256KB | Hard stop threshold |
| Property | Default | Description |
|---|---|---|
ConnectionTimeout |
50000ms | Connect timeout |
PingInterval |
10000ms | Keepalive interval |
ReconnectionPolicy |
null | IReconnectionPolicy; null = disabled |
Authenticate |
null | Func<Memory<byte>> auth token provider |
| Property | Default | Description |
|---|---|---|
AcceptorBacklog |
20 | Listen backlog |
Authenticate |
false | Require client auth |
RateLimiting |
null | ConnectionRateLimitConfig; null = disabled |
AuthorizationCacheDuration |
null | Default auth cache TTL; null = disabled |
DualMode, KeepAlive, TcpKeepAliveTime (-1=OS), TcpKeepAliveInterval, TcpKeepAliveRetryCount, TcpNoDelay (default: true). Server-only: ReuseAddress, ExclusiveAddressUse.
var client = ClientNexus.CreateClient(config, new ClientNexus());
await client.ConnectAsync(); // throws on failure
var result = await client.TryConnectAsync(); // ConnectionResult with .Success, .State, .DisconnectReason
client.StateChanged += (s, state) => { }; // ConnectionState enum
await client.DisconnectedTask; // wait for disconnect
await client.DisconnectAsync();
// Create pipes/channels
var pipe = client.CreatePipe(); // IRentedNexusDuplexPipe
var ch = client.CreateChannel<T>(); // INexusDuplexChannel<T>
var uch = client.CreateUnmanagedChannel<T>(); // INexusDuplexUnmanagedChannel<T>ConnectionState: Unset, Connecting, Connected, Reconnecting, Disconnecting, Disconnected.
// DefaultReconnectionPolicy: retries at 0s, 2s, 10s, 30s then repeats last
config.ReconnectionPolicy = new DefaultReconnectionPolicy();
// Custom intervals
config.ReconnectionPolicy = new DefaultReconnectionPolicy(
[TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5)], continuousRetry: true);var poolConfig = new NexusClientPoolConfig(clientConfig) {
MaxConnections = 10, MaxIdleTime = TimeSpan.FromMinutes(2), MinIdleConnections = 1
};
var pool = new NexusClientPool<ClientNexus, ClientNexus.ServerProxy>(poolConfig);
using var rental = await pool.RentClientAsync();
await rental.Proxy.DoSomething();
await rental.EnsureConnectedAsync(); // reconnect if needed
// Also: pool.GetCollectionConnector(p => p.Items) for relay collectionsvar server = ServerNexus.CreateServer(config, () => new ServerNexus());
await server.StartAsync();
// server.State: Stopped, Running, Disposed
await server.StopAsync();// Broadcasting
await Context.Clients.Caller.Method(); // calling client
await Context.Clients.All.Method(); // all clients
await Context.Clients.Others.Method(); // all except caller
await Context.Clients.Client(id).Method(); // by session ID
await Context.Clients.Clients([id1, id2]).Method(); // multiple IDs
await Context.Clients.Group("room").Method(); // group members
await Context.Clients.Groups(["a", "b"]).Method();
await Context.Clients.GroupExceptCaller("room").Method();
await Context.Clients.GroupsExceptCaller(["a", "b"]).Method();
var ids = Context.Clients.GetIds(); // all session IDs
// Groups
await Context.Groups.AddAsync("room");
await Context.Groups.AddAsync(["room1", "room2"]);
await Context.Groups.RemoveAsync("room");
var names = await Context.Groups.GetNamesAsync();
// Session info
long id = Context.Id;
string? user = Context.Identity?.DisplayName;
// Per-session key-value store (lifetime = connection)
Context.Store["key"] = value;
Context.Store.TryGet("key", out var val);
// Disconnect
await Context.DisconnectAsync();// Invoke clients from outside nexus methods (background services, timers)
using var owner = server.ContextProvider.Rent();
await owner.Context.Clients.All.Notify();
await owner.Context.Clients.Client(sessionId).Notify();
await owner.Context.Clients.Group("room").Notify();var serverConfig = new TcpServerConfig {
EndPoint = endpoint,
RateLimiting = new ConnectionRateLimitConfig {
MaxConcurrentConnections = 1000, // total (default: 1000)
GlobalConnectionsPerSecond = 100, // new conn/sec (default: 100)
MaxConnectionsPerIp = 0, // per-IP concurrent (0=unlimited)
ConnectionsPerIpPerWindow = 0, // per-IP per window (0=unlimited)
PerIpWindowSeconds = 60, // window size (default: 60)
BanDurationSeconds = 300, // ban duration (default: 300)
BanThreshold = 5, // violations before ban (default: 5)
WhitelistedIps = ["127.0.0.1"] // skip rate limiting
}
};Disabled by default. Enable with ServerConfig.Authenticate = true.
// Server config
var serverConfig = new TcpServerConfig { EndPoint = ep, Authenticate = true };
// Server nexus: override OnAuthenticate (return null = reject)
protected override ValueTask<IIdentity?> OnAuthenticate(ReadOnlyMemory<byte>? token) {
var str = Encoding.UTF8.GetString(token!.Value.Span);
return str == "valid" ? new(new DefaultIdentity { DisplayName = "User" }) : new((IIdentity?)null);
}
// Client config
var clientConfig = new TcpClientConfig { EndPoint = ep, Authenticate = () => Encoding.UTF8.GetBytes("valid") };Declarative method/collection authorization via [NexusAuthorize<TPermission>]. Server-only. Permission enum must be backed by int (default).
// 1. Define permission enum
public enum Permission { Read, Write, Admin }
// 2. Decorate methods on the server nexus class
[NexusAuthorize<Permission>(Permission.Admin)]
public ValueTask AdminMethod() { ... }
[NexusAuthorize<Permission>(Permission.Read, Permission.Write)]
public ValueTask MultiPermMethod() { ... }
[NexusAuthorize<Permission>()] // marker-only: requires auth, no specific permission
public ValueTask AnyAuthMethod() { ... }
// 3. Decorate collections on the interface
public partial interface IServerNexus {
[NexusCollection(NexusCollectionMode.ServerToClient)]
[NexusAuthorize<Permission>(Permission.Read)]
INexusList<string> SecureItems { get; }
}
// 4. Override OnAuthorize on the server nexus
protected override ValueTask<AuthorizeResult> OnAuthorize(
ServerSessionContext<ClientProxy> context, int methodId,
string methodName, ReadOnlyMemory<int> requiredPermissions)
{
// requiredPermissions contains int-cast enum values
// Return: Allowed, Unauthorized (error to client), Disconnect (kill session)
var user = context.Identity;
return new(HasPermissions(user, requiredPermissions)
? AuthorizeResult.Allowed : AuthorizeResult.Unauthorized);
}
// 5. Client-side: catch ProxyUnauthorizedException
try { await client.Proxy.AdminMethod(); }
catch (ProxyUnauthorizedException) { /* denied */ }Auth guard runs before deserialization. If OnAuthorize throws, session disconnects (fail-safe). Collections use Disconnect for unauthorized access since they lack a return channel.
Opt-in TTL-based caching per session. Only Allowed and Unauthorized results are cached; Disconnect and exceptions are never cached.
// Server-wide default (null = disabled)
serverConfig.AuthorizationCacheDuration = TimeSpan.FromSeconds(30);
// Per-method override via attribute (-1 = use server config, 0 = never cache, >0 = seconds)
[NexusAuthorize<Permission>(Permission.Read, CacheDurationSeconds = 60)] // 60s override
[NexusAuthorize<Permission>(Permission.Admin, CacheDurationSeconds = 0)] // never cache
[NexusAuthorize<Permission>(Permission.Write)] // use server default
// Explicit invalidation (inside nexus methods)
InvalidateAuthorizationCache(); // clear all cached results
InvalidateAuthorizationCache(methodId); // clear single method| ID | Description |
|---|---|
| NEXNET024 | [NexusAuthorize] on client nexus (server-only) |
| NEXNET025 | [NexusAuthorize] without OnAuthorize override |
| NEXNET026 | Mixed permission enum types across attributes |
| NEXNET027 | Permission enum not backed by int |
NOT thread-safe. For large data or continuous streams.
// Interface method
ValueTask Upload(INexusDuplexPipe pipe);
// Client
var pipe = client.CreatePipe();
await client.Proxy.Upload(pipe);
await pipe.ReadyTask;
await stream.CopyToAsync(pipe.Output);
await pipe.CompleteAsync();
// Server
public async ValueTask Upload(INexusDuplexPipe pipe) {
await pipe.Input.CopyToAsync(destStream);
}Thread-safe writing. INexusDuplexChannel<T> (MemoryPack) or INexusDuplexUnmanagedChannel<T> (unmanaged, faster).
// Interface
ValueTask StreamData(INexusDuplexUnmanagedChannel<int> channel);
// Client
await using var channel = client.CreateUnmanagedChannel<int>();
await client.Proxy.StreamData(channel);
var reader = await channel.GetReaderAsync();
await foreach (var item in reader) { }
// Server
public async ValueTask StreamData(INexusDuplexUnmanagedChannel<int> channel) {
var writer = await channel.GetWriterAsync();
await writer.WriteAsync(42);
await writer.CompleteAsync();
}
// Extensions
await channel.WriteAndComplete(enumerable, batchSize: 100);
var list = await reader.ReadUntilComplete(initialCapacity: 1000);var pipe = client.CreatePipe();
await client.Proxy.StreamData(pipe);
await pipe.ReadyTask;
var writer = await pipe.GetChannelWriter<long>();
var reader = await pipe.GetChannelReader<string>();
// Also: GetUnmanagedChannelWriter/Reader<T>, GetUnmanagedChannel<T>, GetChannel<T>Auto-synced server-to-client. Modes: ServerToClient (read-only client), BiDirectional (client can mutate), Relay (hierarchical).
// Interface
public interface IServerNexus {
[NexusCollection(NexusCollectionMode.BiDirectional)]
INexusList<int> Items { get; }
}
// Client usage
var list = client.Proxy.Items;
await list.ConnectAsync(); // EnableAsync() + ReadyTask
list.Changed.Subscribe(args => { /* Action: Add, Remove, Replace, Move, Reset, Ready */ });
await list.AddAsync(1);
await list.InsertAsync(0, 2);
await list.RemoveAsync(1);
await list.RemoveAtAsync(0);
await list.ReplaceAsync(0, 99);
await list.MoveAsync(0, 1);
await list.ClearAsync();
foreach (var item in list) { } // read local copy
await list.DisableAsync();// Master interface: [NexusCollection(NexusCollectionMode.ServerToClient)]
// Relay interface: [NexusCollection(NexusCollectionMode.Relay)]
var masterPool = new NexusClientPool<MasterClient, MasterClient.ServerProxy>(poolConfig);
var relayServer = RelayNexus.CreateServer(config, () => new RelayNexus(),
cfg => cfg.Context.Collections.Items.ConfigureRelay(
masterPool.GetCollectionConnector(p => p.Items)));Server-only. All methods need [NexusMethod(id)] with unique IDs. HashLock prevents accidental changes.
[NexusVersion(Version = "v1.0", HashLock = -2031775281)]
public interface IServerV1 {
[NexusMethod(1)] ValueTask<bool> GetStatus();
}
[NexusVersion(Version = "v2.0", HashLock = -1210855623)]
public interface IServerV2 : IServerV1 {
[NexusMethod(2)] ValueTask<string> GetInfo();
}
// V2 server supports V1+V2 clients
[Nexus<IServerV2, IClient>(NexusType = NexusType.Server)]
public partial class ServerV2 { ... }Attribute options: [NexusMethod(Ignore = true)], [NexusCollection(Id = 1)], [NexusCollection(Ignore = true)].
Requires NexNet.Asp.
builder.Services.AddNexusServer<ServerNexus, ServerNexus.ClientProxy>();
app.UseAuthentication();
app.UseAuthorization();
// HttpSocket
await app.UseHttpSocketNexusServerAsync<ServerNexus, ServerNexus.ClientProxy>(c => {
c.NexusConfig.Path = "/nexus";
c.NexusConfig.AspEnableAuthentication = true;
c.NexusConfig.AspAuthenticationScheme = "BearerToken";
c.NexusConfig.TrustProxyHeaders = false; // X-Forwarded-For (default: false)
}).StartAsync(app.Lifetime.ApplicationStopped);
// WebSocket: UseWebSocketNexusServerAsync insteadconfig.Logger = new ConsoleLogger(); // stdout
config.Logger = new RollingLogger(maxLines: 200); // circular buffer, Flush(TextWriter)
// NexusLogLevel: Trace, Debug, Information, Warning, Error, Critical, None
// NexusLogBehaviors flags: Default, ProxyInvocationsLogAsInfo, LocalInvocationsLogAsInfo, LogTransportData// Must be last parameter in interface
ValueTask Operation(int data, CancellationToken ct);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await client.Proxy.Operation(42, cts.Token);