Hub Invocations
NexNet supports several method invocation patterns between server and client. The source generator handles all proxy creation — you define interfaces, and the generator emits the invocation infrastructure.
Return Types
Nexus interface methods support three return types:
| Return Type | Behavior |
|---|---|
void |
Fire-and-forget. The caller does not wait for completion. Use for notifications. |
ValueTask |
The caller awaits completion but receives no return value. |
ValueTask<T> |
The caller awaits and receives a return value from the remote method. |
The ValueTask a call returns follows the usual ValueTask rules: consume it once, by awaiting it, blocking on it, or converting it with AsTask(). It is backed by pooled state that is reused once the result has been read, so consuming it a second time throws InvalidOperationException. Call AsTask() first if the result is needed more than once.
public interface IServerNexus
{
void Notify(string message); // Fire-and-forget
ValueTask UpdateAndWait(int id, string data); // Await completion
ValueTask<int> GetStatus(int userId); // Await with result
}
Method Invocation Compatibility
Some argument types have restrictions on how they can be combined. The source generator enforces these rules with compile-time diagnostics.
| CancellationToken | INexusDuplexPipe | INexusDuplexChannel<T> | Args | |
|---|---|---|---|---|
void |
X | |||
ValueTask |
X | X | ||
ValueTask |
X | X | X | |
ValueTask<T> |
X | X |
Rules:
CancellationTokencannot be combined withINexusDuplexPipeorINexusDuplexChannel<T>because pipes and channels have built-in cancellation/completion notifications.CancellationTokenmust be the last parameter, following standard .NET conventions.- Arguments and results may each be up to
ConfigBase.MaxMessageSize(4 MiB by default, the smaller of both sides' settings). Values larger than one 64 KiB frame are streamed automatically. A larger value fails the call withNexusMessageTooLargeException, and a value the receiver has no memory for right now fails it withNexusBusyException; the session stays connected in both cases. For larger or continuous data, use Duplex Pipes or Channels. - The receiving side queues a limited number of invocations per session (
ConfigBase.InvocationCredits) and runsMaxConcurrentConnectionInvocationsof them at a time. When the queue is full, a call waits until a slot frees up. Methods marked[NexusMethod(SeparatePool = true)]run in a separate pool instead (see Slow methods). See Limits and Flow Control. - Argument and return types must be serializable: built-in types,
[NexusObject]types, or types with a registered formatter. See Serialization.
Slow methods
A plain method holds one of the session's MaxConcurrentConnectionInvocations execution slots until it returns, so a slow one (a git fetch and merge, a long query: seconds to minutes) makes the fast calls on the same connection queue behind it. Mark such a method SeparatePool = true:
public interface IServerNexus
{
[NexusMethod(SeparatePool = true)]
ValueTask<int> SyncRepository(string remote, CancellationToken ct);
ValueTask<Status> GetStatus();
}
Its invocations run in a second pool of the session, with its own queue and ConfigBase.MaxSeparatePoolInvocations slots (4 by default, 16 with NexusSerializerOptions.Trusted). They never take or wait for a MaxConcurrentConnectionInvocations slot, and plain calls never wait for a separate-pool slot. Both queues share the session's InvocationCredits. Cancellation works as for any other method, while queued or running.
- Not
TaskCreationOptions.LongRunning. No dedicated thread is created: the method runs on the thread pool like every other invocation. Only the queue and the cap differ. - The method must be truly asynchronous. Await the slow work (
await process.WaitForExitAsync(ct), notprocess.WaitForExit()or.Result): a method that blocks synchronously holds a thread-pool thread for its whole run, and many of them starve the pool for every session. - The cap is per session, not server-wide: 100 clients may each run
MaxSeparatePoolInvocationssuch calls at once. Limit server-wide work in the method itself (aSemaphoreSlim, a work queue) when that matters. - The executing side decides: the flag is read from its own nexus and changes neither the method hash nor anything on the wire, so peers with and without it stay compatible.
- No effect on a method with a pipe or channel parameter, which frees its slot once the pipe is registered (warning NEXNET039), nor on a collection property.
Cancellation
Methods that return ValueTask or ValueTask<T> can accept a CancellationToken as the last parameter:
public interface IServerNexus
{
ValueTask<byte[]> DownloadFile(string path, CancellationToken ct);
}
Cancelling the token on the caller side propagates the cancellation to the remote method. Each call is canceled individually, also when several calls to the same method are running. A remote method that throws OperationCanceledException on its own, with or without a token, fails the call with TaskCanceledException.
The cancellation reaches the remote method only through its own CancellationToken parameter: a method without one runs to completion even if the caller cancels. This also holds while the invocation is still queued for an execution slot (see MaxConcurrentConnectionInvocations): a call cancelled while queued still starts, with its token already cancelled, so the method can return at once.
When a method is invoked without waiting for its result (a broadcast, or a ValueTask method invoked on several clients), the caller can't cancel it. Its token is canceled when the session ends.
Errors
When a ValueTask or ValueTask<T> method throws on the side that runs it, the caller's call fails with ProxyRemoteInvocationException. The executing side logs the exception; by default nothing about it is sent, and the exception's message is Exception occurred while executing method on proxy.
What is sent is decided by the side that runs the method: a server's configuration controls what its clients see, a client's what the server sees when it calls the client.
ConfigBase.RemoteExceptionDetail = RemoteExceptionDetail.TypeAndMessagesends the exception's full type name and its message.- Overriding
OnInvocationExceptionon the nexus sends an application error instead, whatever the setting says. Return null to fall back to the setting. An exception thrown by the hook is logged and treated as null.
partial class ServerNexus
{
protected override NexusRemoteError? OnInvocationException(Exception exception, int methodId)
=> exception switch
{
KeyNotFoundException => new NexusRemoteError(404, "Not found."),
_ => null
};
}
On the caller, the exception exposes what was sent:
| Property | Value |
|---|---|
RemoteErrorCode |
The hook's code, or null |
RemoteExceptionType |
The exception's full type name with TypeAndMessage, else null |
RemoteMessage |
The exception's message with TypeAndMessage, the hook's message, or null |
The type is truncated to 256 characters and the message to 2048. A detail that is malformed or longer than that is ignored, and the caller gets the plain exception. Fire-and-forget (void) methods, broadcasts, and pipe and channel methods send nothing. Canceled and unauthorized calls are unchanged (TaskCanceledException, ProxyUnauthorizedException). A peer without this feature ignores the detail.
Warning
TypeAndMessage can reveal internal details (file paths, connection strings, user data) to the peer. With untrusted peers, leave it at None and return application error codes from OnInvocationException.
Execution Context
Invoked methods run on the thread pool, in the execution context of the code that started the session: on a client the code that called ConnectAsync, on a server the code that called StartAsync or, with the ASP.NET Core transports, the upgrade request. Methods therefore see its AsyncLocal values, such as IHttpContextAccessor.HttpContext (when registered with AddHttpContextAccessor), Activity.Current, logging scopes and CultureInfo.CurrentCulture. Each invocation runs in its own copy, so values one method sets are not seen by other invocations.
Server Broadcasting
The server can invoke methods on multiple connected clients with a single call through the session context:
[Nexus<IServerNexus, IClientNexus>(NexusType = NexusType.Server)]
partial class ServerNexus
{
public async ValueTask BroadcastStatus(int status)
{
// Send to all connected clients
await Context.Clients.All.OnStatusChanged(status);
// Send to a specific group
await Context.Clients.Group("room-1").OnStatusChanged(status);
}
}
A broadcast is written straight away to every client that can take it; clients that are behind are waited for together, for at most ConfigBase.ProgressTimeout, so a slow client does not delay the others. See Limits and Flow Control.
See Also
- Sessions & Lifetimes — Session groups and broadcasting targets
- Duplex Pipes — Streaming large data that exceeds the argument size limit
- Channels — Type-safe data streaming
- Serialization — Making argument and return types serializable