Table of Contents

Method name transforms

By default, Nerdbank.JsonRpc maps CLR method names to JSON-RPC wire names by removing a trailing Async suffix (if any) and converting the result to camelCase. For example, GetValueAsync becomes getValue and Ping becomes ping. This applies symmetrically to server-side target registration (AddRpcTarget) and generated client proxies (Attach), so a contract's methods dispatch under the same wire name on both sides without any extra configuration.

[JsonRpcContract]
[GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
public partial interface ICalculator
{
    // Sent on the wire as "add" by default: the trailing "Async" is removed and the
    // remainder is camelCased.
    ValueTask<int> AddAsync(int a, int b, CancellationToken cancellationToken);

    // An explicit name is authoritative and is used verbatim, bypassing any configured transform.
    [MethodShape(Name = "subtract")]
    ValueTask<int> SubtractAsync(int a, int b, CancellationToken cancellationToken);
}

Explicit names are authoritative

A method annotated with [MethodShape(Name = "...")] always dispatches on that exact name, both when registering a server target and when generating a client proxy. The configured transform is not applied to it. This makes explicit names a reliable escape hatch when you need a specific wire name, such as subtract in the example above.

Every method on a target must have a unique wire name, including when signatures accept different argument counts. Contracts annotated with JsonRpcContractAttribute or RpcMarshalableAttribute receive NBJSONRPC002 when their names collide under the default policy.

Interoperating with StreamJsonRpc

StreamJsonRpc's default behavior is to send and expect CLR-style method names (for example, GetValueAsync) unless its own MethodNameTransform is configured. To interoperate with such a peer, use Identity on either side, which returns names unchanged:


// Interoperate with a StreamJsonRpc peer that hasn't configured its own method name transform:
// dispatch on the CLR names it sends by default (e.g. "AddAsync"). This is an alternative
// to the default registration above, not a second registration on the same JsonRpc instance.
if (useIdentityTargetNaming)
{
    rpc.AddRpcTarget<ICalculator>(calculator, new JsonRpcTargetOptions { MethodNameTransform = CommonMethodNameTransforms.Identity });
}

// Send CLR-style names (e.g. "AddAsync") to interoperate with a StreamJsonRpc peer
// that hasn't configured its own method name transform.
ICalculator interopClient = rpc.Attach<ICalculator>(new JsonRpcProxyOptions { MethodNameTransform = CommonMethodNameTransforms.Identity });

Marshaled objects

RPC-marshalable objects are registered as targets and attached as proxies implicitly. By default their methods use the same name transform as ordinary RPC contracts: the trailing Async suffix is removed and the name is camel-cased. Their naming options are independent of options supplied to an individual AddRpcTarget or Attach call.

StreamJsonRpc instead uses verbatim CLR names for RPC-marshalable interfaces. For interoperability with its default configuration, set both MarshaledTarget and MarshaledProxy in the options passed to the connection constructor:

JsonRpcOptions options = new()
{
    MarshaledTarget = new() { MethodNameTransform = CommonMethodNameTransforms.Identity },
    MarshaledProxy = new() { MethodNameTransform = CommonMethodNameTransforms.Identity },
};
JsonRpc rpc = new(channel, options);

These properties affect only RPC-marshalable objects, not ordinary RPC targets or proxies. Set the options for your ordinary contract separately if it also needs to use StreamJsonRpc's naming convention. [MethodShape(Name = "...")] still takes precedence over any transform.

Custom transforms

Set MethodNameTransform or MethodNameTransform to a Func<string, string> to fully control implicit name mapping. The function receives the CLR method name. It must return a non-null, non-empty result; registering a target throws if two methods transform to the same wire name, or if the transform returns a null or empty value.

Events use the separate EventNameTransform, which defaults to camelCase only and preserves a trailing Async suffix. Unlike method naming, the default event transform does not remove Async; see Events as notifications.