RMI (Remote Method Invocation) — Advanced
For a basic introduction see the RMI example in Quick start.
Registering and resolving controllers
Section titled “Registering and resolving controllers”// Register ahead of timeserver.Use<IChatServer, ChatServer>(client => new ChatServer { Client = client, Server = server });
// Or resolve lazily on first useserver.ControllerResolve += (s, e) =>{ if (e.ControllerType == typeof(IChatServer)) e.Controller = new ChatServer { Client = e.Client, Server = server };};ControllerResolve fires the first time a remote method call arrives targeting a controller type that hasn’t been registered with Use<TInterface, TImplementation>(). This lets you create controller instances on demand (e.g. with dependency injection) instead of registering every controller type upfront.
The event args type differs depending on where you subscribe:
- On
Client/ServerClient(which both derive from the internalNetworkClientbase), the event isEventHandler<NetworkConnectionControllerResolveEventArgs>with properties:string ControllerTypeName— assembly-qualified-style name of the requested controller interface.Type ControllerType— the resolvedType, ornullif it couldn’t be loaded viaType.GetType(...).object Controller— set this to the instance you want to use.
- On
Server, the event isEventHandler<ServerClientControllerResolveEventArgs>, which additionally exposesServerClient Client(the specific client the call came from) via its baseServerClientEventArgs.
There is no Handled/Cancel flag — the first subscriber (in the order events were added) that sets a non-null Controller wins, and the rest are skipped.
Supported method signatures
Section titled “Supported method signatures”Based on the documented examples, controller methods can:
- Take primitive types or serializable objects as parameters.
- Return
void,Task, or any primitive/serializable type (includingTask<T>).
public interface IMyController{ void FireAndForget(string message); Task<int> ComputeAsync(int a, int b);}Overloads are supported — the remote call is matched by method name and exact parameter type list. The following are explicitly not supported and should be avoided:
- Generic methods (methods with their own type parameters, e.g.
T Echo<T>(T value)). Using closed generic types as ordinary parameters/return values (e.g.List<string>) works fine — it’s only open type parameters on the method itself that aren’t supported. ref/outparameters — arguments are captured and sent once when the call is made; any value the remote implementation writes to aref/outparameter is not sent back and will not be visible to the caller.CancellationTokenparameters — a token has no meaningful representation once serialized to the remote side; cancelling a token locally will not cancel the corresponding remote call.
Exception propagation
Section titled “Exception propagation”Exceptions thrown inside a controller method on the remote side propagate back to the caller, but always wrapped in NetworkControllerInvocationException — the original exception type is never rethrown as-is:
public class ChatServer : IChatServer{ public void Send(string message) { if (string.IsNullOrEmpty(message)) throw new ArgumentException("Message cannot be empty."); // ... }}try{ client.GetController<IChatServer>().Send(message);}catch (NetworkControllerInvocationException ex){ // ex.Message is a generic "Remote controller execution exception occurred." Console.WriteLine(ex.Type); // NetworkControllerInvocationExceptionType.MethodInvokeException Console.WriteLine(ex.RemoteMessage); // the original exception's Message ("Message cannot be empty.") Console.WriteLine(ex.RemoteException);// the original exception's full ToString() (including stack trace)}NetworkControllerInvocationException.Type (NetworkControllerInvocationExceptionType) tells you why the call failed:
| Value | Meaning |
|---|---|
MethodInvokeException |
The registered controller method threw — RemoteMessage/RemoteException describe the original exception. |
ControllerNotFound |
No controller was registered/resolved for the requested interface on the remote side. |
MethodNotFound |
The remote controller exists, but no method matched the requested name + parameter types. |
DataReceivingError |
The response could not be read/deserialized (e.g. serializer mismatch between the two sides). |
OperationCancelled |
The connection was disposed while the call was still waiting for a response. |
Lifecycle of controller instances
Section titled “Lifecycle of controller instances”The controller factory delegate passed to Use<TInterface, TImplementation>(factory) is invoked once, the first time a remote call targets that controller type on a given connection. The resulting instance is then cached for the lifetime of that connection — subsequent calls reuse the same instance rather than recreating it. This is safe to rely on if your controller holds per-connection state (as ChatServer does with its Client/Server properties in the example above).