Events as notifications
Registering a target object also wires up any of its public instance events: raising an event on the target automatically sends a JSON-RPC notification to the remote party, using the same PolyType shape metadata used for method dispatch.
[GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
public partial class Watcher
{
// The classic .NET event pattern: the "sender" parameter is recognized and omitted from the
// notification, so only the EventArgs-derived value is sent. Sent on the wire as "priceChanged"
// by default (the same camelCase transform used for method names).
public event EventHandler<decimal>? PriceChanged;
// A custom delegate without a leading "sender" parameter forwards all of its parameters,
// positionally, as the notification's arguments.
public event Action<int, int>? RangeChanged;
public void RaisePriceChanged(decimal price) => this.PriceChanged?.Invoke(this, price);
public void RaiseRangeChanged(int low, int high) => this.RangeChanged?.Invoke(low, high);
}
// Raising PriceChanged or RangeChanged on `watcher` after this call sends a
// "priceChanged" or "rangeChanged" notification to the remote party.
if (!disableEventForwarding)
{
rpc.AddRpcTarget(watcher);
}
Argument mapping
- Sender exclusion applies only to the BCL's
EventHandlerandEventHandler<TEventArgs>delegate types specifically — not to any delegate that merely happens to share their(object? sender, TEventArgs e)shape. A custom delegate with that same signature is treated like any other delegate: both parameters are forwarded. - Any other delegate shape sends all of its parameters, positionally, as the notification's arguments.
- Only synchronous,
void-returning delegates are supported. Registering a target with an event of another shape (for example, aTask-returning delegate) throws NotSupportedException. - Static events are ignored, since there is no single target instance to associate handler subscription and removal with.
Naming
Event names use their own EventNameTransform, which defaults to CommonMethodNameTransforms.CamelCase and converts to camelCase without removing an Async suffix. This is separate from the method-name transform, which removes a trailing Async suffix by default. An explicit [EventShape(Name = "...")] is authoritative and bypasses the event transform. See Method name transforms for more on how transforms and explicit names interact.
Opting out
Set NotifyClientOfEvents to false to register a target's methods without subscribing to its events:
// Disable event forwarding for a target: raising its events no longer sends notifications.
// This is an alternative to the default registration above, not a second registration
// on the same JsonRpc instance.
if (disableEventForwarding)
{
rpc.AddRpcTarget(watcher, new JsonRpcTargetOptions { NotifyClientOfEvents = false });
}
Receiving the notifications
A notification is just a request without an id, so the remote party receives it the same way it would any other RPC call: by registering a target object whose method names and parameter shapes match the notification. Method name mapping works exactly like regular RPC methods — the default camelCase transform turns a method named PriceChanged into the wire name priceChanged, matching the notification sent above.
Because it's an ordinary RPC method, a receiving method may also be declared asynchronously: naming it PriceChangedAsync (with the trailing Async stripped by the default MethodNameTransform, just like any other method) and returning Task or ValueTask both work. The notification carries no reply, so the returned task's result (if any) is discarded, but the method still runs to completion and any exception it throws is logged the same way a fire-and-forget request's would be. Since the default event-name transform preserves Async, an event whose CLR name ends in Async will not automatically match a same-named receiver method; configure the event or receiver transform (or an explicit event name) so both sides use the same wire name.
[GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
public partial class WatcherEventReceiver
{
// Matches the "priceChanged" notification: a method named "PriceChanged" (camelCased by default,
// just like a regular RPC method) with a single parameter of the forwarded argument's type.
public void PriceChanged(decimal price)
{
// Handle the price change here.
}
// Matches the "rangeChanged" notification: both of RangeChanged's forwarded parameters are
// received positionally, by name or position depending on how the sender formats arguments.
// A receiving method may also be asynchronous, as here: the trailing "Async" is stripped by the
// default method name transform (just like any other RPC method), and its result is discarded
// since notifications carry no reply, but the method still runs to completion.
public async Task RangeChangedAsync(int low, int high)
{
// Handle the range change here.
await Task.CompletedTask;
}
}
public static void RegisterReceiver(JsonRpc remoteRpc, WatcherEventReceiver receiver)
{
ArgumentNullException.ThrowIfNull(remoteRpc);
ArgumentNullException.ThrowIfNull(receiver);
// On the remote party's JsonRpc instance, register a target whose methods match the
// notifications' wire names and parameter shapes to receive them.
remoteRpc.AddRpcTarget(receiver);
}