From 77fb2697ec64259eb095e996d0723b3afafd2a47 Mon Sep 17 00:00:00 2001 From: Austin Harris Date: Thu, 24 Sep 2026 23:05:46 -0600 Subject: [PATCH] API vocabulary: MethodInfo spelled out, sessionId everywhere, legacy registration obsolete "method" is the JSON-RPC callable named on the wire and a MethodInfo is always spelled MethodInfo: RpcMethod.FromMethod is FromMethodInfo and RpcInterfaceMethod.Method is MethodInfo. The session parameter is spelled sessionId on every overload; BindService, Handler.RegisterInstance and the JsonRpcService(string) constructor used sessionID. Handler gets a class summary and SessionSelector's summary says the selection is independent of ASP.NET Core session state. Handler.RegisterFuction and UnRegisterFunction keep their names and behaviour and are obsolete; the message says that BindMethod throws on a duplicate name where RegisterFuction replaces it. Tests that exercise the legacy pair suppress CS0618 locally. The README defines "method" at the top of Defining methods and "session" at the top of Sessions and context; the package README gets the method sentence; CHANGELOG and the upgrade guide record the renames. --- .../JsonRpcOptions.cs | 5 +- .../AsyncInvocationTests.cs | 6 ++- .../DelegateBindingTests.cs | 2 + AustinHarris.JsonRpcTestN/DiLifetimeTests.cs | 2 +- .../DispatchHardeningTests.cs | 2 + .../InterfaceBindingTests.cs | 2 +- AustinHarris.JsonRpcTestN/NewtonsoftTests.cs | 2 + .../SessionAndConfigTests.cs | 2 + AustinHarris.JsonRpcTestN/Test.cs | 6 +++ CHANGELOG.md | 3 ++ Json-Rpc/Handler.cs | 11 +++-- Json-Rpc/Invocation/RpcMethod.cs | 20 ++++---- Json-Rpc/JsonRpcService.cs | 6 +-- Json-Rpc/README.md | 2 + Json-Rpc/RpcInterfaceBindingOptions.cs | 8 ++-- Json-Rpc/ServiceBinder.cs | 48 +++++++++---------- README.md | 10 ++-- docs/upgrading.md | 2 + 18 files changed, 85 insertions(+), 54 deletions(-) diff --git a/AustinHarris.JsonRpc.AspNetCore/JsonRpcOptions.cs b/AustinHarris.JsonRpc.AspNetCore/JsonRpcOptions.cs index 353ac64..1a22f8a 100644 --- a/AustinHarris.JsonRpc.AspNetCore/JsonRpcOptions.cs +++ b/AustinHarris.JsonRpc.AspNetCore/JsonRpcOptions.cs @@ -16,8 +16,9 @@ public class JsonRpcOptions public string SessionId { get; set; } /// - /// Picks the session per HTTP request (for example from a route value or a header). When set it takes - /// precedence over . Not used by the raw connection handler. + /// Selects the JSON-RPC session id for this HTTP request, for example from a route value or a header; + /// selection is independent of ASP.NET Core session state unless the callback explicitly uses it. + /// When set it takes precedence over . Not used by the raw connection handler. /// public Func SessionSelector { get; set; } diff --git a/AustinHarris.JsonRpcTestN/AsyncInvocationTests.cs b/AustinHarris.JsonRpcTestN/AsyncInvocationTests.cs index eb4fd67..091b824 100644 --- a/AustinHarris.JsonRpcTestN/AsyncInvocationTests.cs +++ b/AustinHarris.JsonRpcTestN/AsyncInvocationTests.cs @@ -140,7 +140,9 @@ public async Task CompatibilityRegistrationSurfaces_SupportAsyncMethods(int surf var method = new Func>(() => Task.FromResult(7)); var handler = Handler.GetSessionHandler(_session); if (surface == 0) handler.MetaData.Services["run"] = new SMDService("POST", "JSON-RPC-2.0", types, new Dictionary(), method); +#pragma warning disable CS0618 else if (surface == 1) handler.RegisterFuction("run", types, null, method); +#pragma warning restore CS0618 else _ = new AutoAsyncService(_session); Assert.AreEqual(7, (int)JObject.Parse(await Run(Request("run")))["result"]); Assert.AreEqual(typeof(int), handler.MetaData.Services["run"].Method.ResultType); @@ -172,13 +174,15 @@ public void AsyncVoid_IsRejectedOnEverySurface(int surface) var types = new Dictionary { ["returns"] = typeof(void) }; TestDelegate registration = surface switch { - 0 => () => RpcMethod.FromMethod("invalid", typeof(InvalidService).GetMethod("Invalid"), new InvalidService()), + 0 => () => RpcMethod.FromMethodInfo("invalid", typeof(InvalidService).GetMethod("Invalid"), new InvalidService()), 1 => () => RpcMethod.FromDelegate("invalid", invalid), 2 => () => ServiceBinder.BindService(_session, new InvalidService()), 3 => () => Bind("invalid", invalid), 4 => () => new InvalidAutoService(_session), 5 => () => new SMDService("POST", "JSON-RPC-2.0", types, new Dictionary(), invalid), +#pragma warning disable CS0618 _ => () => Handler.GetSessionHandler(_session).RegisterFuction("invalid", types, null, invalid) +#pragma warning restore CS0618 }; StringAssert.Contains("async void", Assert.Throws(registration).Message); } diff --git a/AustinHarris.JsonRpcTestN/DelegateBindingTests.cs b/AustinHarris.JsonRpcTestN/DelegateBindingTests.cs index af1cc59..ae58f72 100644 --- a/AustinHarris.JsonRpcTestN/DelegateBindingTests.cs +++ b/AustinHarris.JsonRpcTestN/DelegateBindingTests.cs @@ -128,7 +128,9 @@ public void Names_MustBeFree_AndUnbindFreesThem() Assert.AreEqual("{\"jsonrpc\":\"2.0\",\"result\":2,\"id\":1}", Run("{\"method\":\"m\",\"id\":1}")); // the legacy surface keeps replacing silently +#pragma warning disable CS0618 Handler.GetSessionHandler(Session).RegisterFuction("m", new Dictionary { ["returns"] = typeof(int) }, null, new Func(() => 3)); +#pragma warning restore CS0618 Assert.AreEqual("{\"jsonrpc\":\"2.0\",\"result\":3,\"id\":1}", Run("{\"method\":\"m\",\"id\":1}")); } diff --git a/AustinHarris.JsonRpcTestN/DiLifetimeTests.cs b/AustinHarris.JsonRpcTestN/DiLifetimeTests.cs index bd729c7..17a0e31 100644 --- a/AustinHarris.JsonRpcTestN/DiLifetimeTests.cs +++ b/AustinHarris.JsonRpcTestN/DiLifetimeTests.cs @@ -640,7 +640,7 @@ public void Arguments_AreChecked() Assert.Throws(() => ServiceBinder.BindService(_session, (object)null)); Assert.Throws(() => ServiceBinder.BindService(_session, typeof(List<>), c => null)); var method = typeof(TaggedService).GetMethod(nameof(TaggedService.Tag)); - var ex = Assert.Throws(() => AustinHarris.JsonRpc.Invocation.RpcMethod.FromMethod("fb.tag", method, typeof(string), c => null)); + var ex = Assert.Throws(() => AustinHarris.JsonRpc.Invocation.RpcMethod.FromMethodInfo("fb.tag", method, typeof(string), c => null)); StringAssert.Contains(typeof(TaggedService).FullName, ex.Message); } } diff --git a/AustinHarris.JsonRpcTestN/DispatchHardeningTests.cs b/AustinHarris.JsonRpcTestN/DispatchHardeningTests.cs index 77d5632..439f30c 100644 --- a/AustinHarris.JsonRpcTestN/DispatchHardeningTests.cs +++ b/AustinHarris.JsonRpcTestN/DispatchHardeningTests.cs @@ -154,7 +154,9 @@ public void BindServices() public void DestroySessions() { Handler.DestroySession(Session); +#pragma warning disable CS0618 Handler.DefaultHandler.UnRegisterFunction("dh.whichSession"); +#pragma warning restore CS0618 } private static string Run(string json, object context = null, JsonRpcSerializer serializer = null, string session = Session) diff --git a/AustinHarris.JsonRpcTestN/InterfaceBindingTests.cs b/AustinHarris.JsonRpcTestN/InterfaceBindingTests.cs index 84d77d7..322d963 100644 --- a/AustinHarris.JsonRpcTestN/InterfaceBindingTests.cs +++ b/AustinHarris.JsonRpcTestN/InterfaceBindingTests.cs @@ -261,7 +261,7 @@ public void Include_ReadsHostAttributeForEveryAlias() var seen = new List(); using var binding = ServiceBinder.BindInterface(Session, new Aliases(), new RpcInterfaceBindingOptions { - Include = method => { seen.Add(method); return method.Method.IsDefined(typeof(ExportAttribute), false); } + Include = method => { seen.Add(method); return method.MethodInfo.IsDefined(typeof(ExportAttribute), false); } }); Assert.AreEqual(3, seen.Count); CollectionAssert.AreEquivalent(new[] { "ALIAS", "OtherAlias" }, binding.Methods); diff --git a/AustinHarris.JsonRpcTestN/NewtonsoftTests.cs b/AustinHarris.JsonRpcTestN/NewtonsoftTests.cs index 58aa357..8075e64 100644 --- a/AustinHarris.JsonRpcTestN/NewtonsoftTests.cs +++ b/AustinHarris.JsonRpcTestN/NewtonsoftTests.cs @@ -93,7 +93,9 @@ public void Settings_PerSession_OverridesGlobal() try { var h = Handler.GetSessionHandler(sessionId); +#pragma warning disable CS0618 h.RegisterFuction("echo", new System.Collections.Generic.Dictionary { { "s", typeof(string) }, { "returns", typeof(string) } }, null, new Func(s => s)); +#pragma warning restore CS0618 h.Serializer = new NewtonsoftJsonRpcSerializer(new JsonSerializerSettings { Converters = { new ShoutingStringConverter() } }); // four arguments on purpose: ProcessSync(sessionId, json, null) binds to the (jsonRpc, context, serializer) overload var result = JsonRpcProcessor.ProcessSync(sessionId, "{\"method\":\"echo\",\"params\":[\"abc\"],\"id\":1}", null, null); diff --git a/AustinHarris.JsonRpcTestN/SessionAndConfigTests.cs b/AustinHarris.JsonRpcTestN/SessionAndConfigTests.cs index 2bc01e1..533ea73 100644 --- a/AustinHarris.JsonRpcTestN/SessionAndConfigTests.cs +++ b/AustinHarris.JsonRpcTestN/SessionAndConfigTests.cs @@ -250,8 +250,10 @@ public void JsonRpcService_AutoBindFalse_BindsNowhere_UntilBoundExplicitly() } finally { +#pragma warning disable CS0618 Handler.DefaultHandler.UnRegisterFunction("sc.bound"); Handler.GetSessionHandler(Session).UnRegisterFunction("sc.unbound"); +#pragma warning restore CS0618 } } } diff --git a/AustinHarris.JsonRpcTestN/Test.cs b/AustinHarris.JsonRpcTestN/Test.cs index 001fa3c..adbf501 100644 --- a/AustinHarris.JsonRpcTestN/Test.cs +++ b/AustinHarris.JsonRpcTestN/Test.cs @@ -95,7 +95,9 @@ public void TestCanCreateAndRemoveSession() Tuple.Create ("sooper", typeof(string)), Tuple.Create ("returns", typeof(string)) }.ToDictionary(x => x.Item1, x => x.Item2); +#pragma warning disable CS0618 h.RegisterFuction("workie", metadata, new System.Collections.Generic.Dictionary(),new Func(x => "workie ... " + x)); +#pragma warning restore CS0618 string request = @"{""method"":""workie"",""params"":{""sooper"":""good""},""id"":1}"; string expectedResult = "{\"jsonrpc\":\"2.0\",\"result\":\"workie ... good\",\"id\":1}"; @@ -1612,7 +1614,9 @@ public void TestPreProcessOnSession() Tuple.Create ("sooper", typeof(string)), Tuple.Create ("returns", typeof(string)) }.ToDictionary(x => x.Item1, x => x.Item2); +#pragma warning disable CS0618 h.RegisterFuction("workie", metadata, new System.Collections.Generic.Dictionary(),new Func(x => "workie ... " + x)); +#pragma warning restore CS0618 string request = @"{""method"":""workie"",""params"":{""sooper"":""good""},""id"":1}"; string expectedResult = "{\"jsonrpc\":\"2.0\",\"result\":\"workie ... good\",\"id\":1}"; @@ -1852,7 +1856,9 @@ public void TestPostProcessOnSession() Tuple.Create ("sooper", typeof(string)), Tuple.Create ("returns", typeof(string)) }.ToDictionary(x => x.Item1, x => x.Item2); +#pragma warning disable CS0618 h.RegisterFuction("workie", metadata, new System.Collections.Generic.Dictionary(), new Func(x => "workie ... " + x)); +#pragma warning restore CS0618 string request = @"{""method"":""workie"",""params"":{""sooper"":""good""},""id"":1}"; string expectedResult = "{\"jsonrpc\":\"2.0\",\"result\":\"workie ... good\",\"id\":1}"; diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f4b2e3..ae54cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,9 @@ behaviour: a breaking change to either means a new major version. ### Changed +- `RpcMethod.FromMethod` is `RpcMethod.FromMethodInfo`; `RpcInterfaceMethod.Method` is `RpcInterfaceMethod.MethodInfo`. A `MethodInfo` is always spelled out; "method" means the JSON-RPC method. +- The session parameter is spelled `sessionId` on every overload (`BindService`, `Handler.RegisterInstance` and the `JsonRpcService` constructor used `sessionID`). +- `Handler.RegisterFuction` and `UnRegisterFunction` are obsolete; use `ServiceBinder.BindMethod` and `UnbindMethod` (which throw on a duplicate name instead of replacing it). - The core no longer depends on Json.NET. - `ProcessAsync` no longer serializes the process on one lock per document. Each thread now caches one async scratch (input copy, reader, staged output) in front of the shared pool, which handles only misses and overflow. At 16 workers, the inline rows went from about 4 M to 22.2 M to 32.1 M RPC/s across the registrations, and the row with a real suspension from 3.9 M to 8.96 M (one run per row, 2026-09-25). The library retains one scratch per thread that has run `ProcessAsync` plus 64 shared, with buffers at most 64 KiB each. - The request path looks sessions up without creating them. A request for a session id that was never registered answers `-32601` for every call and leaves the registry untouched; sessions are created by binding and by the per-session `Config` setters. Registration adds the session before publishing the registry version, so a thread that misses its snapshot consults the master registry and cannot answer `-32601` for a session that exists. diff --git a/Json-Rpc/Handler.cs b/Json-Rpc/Handler.cs index ae013d7..7933d00 100644 --- a/Json-Rpc/Handler.cs +++ b/Json-Rpc/Handler.cs @@ -10,6 +10,7 @@ namespace AustinHarris.JsonRpc using AustinHarris.JsonRpc.Serialization; using System.Collections.Concurrent; + /// Dispatches requests for one session: a named set of JSON-RPC methods and configuration whose lifetime is managed explicitly. public sealed partial class Handler { #region Members @@ -112,7 +113,7 @@ public static Handler GetSessionHandler() } /// - /// Removes and clears the Handler with the specific sessionID from the registry of Handlers + /// Removes and clears the Handler with the specific sessionId from the registry of Handlers /// public static void DestroySession(string sessionId) { @@ -135,7 +136,7 @@ public void Destroy() public static Handler DefaultHandler { get { return GetSessionHandler(_defaultSessionId); } } /// - /// The sessionID of this Handler + /// The sessionId of this Handler /// public string SessionId { get; private set; } @@ -260,9 +261,9 @@ public static JsonRpcException RpcGetAndRemoveRpcException() /// /// Allows you to register all the functions on a Pojo Type that have been attributed as [JsonRpcMethod] to the specified sessionId /// - public static void RegisterInstance(string sessionID, object instance) + public static void RegisterInstance(string sessionId, object instance) { - ServiceBinder.BindService(sessionID, instance); + ServiceBinder.BindService(sessionId, instance); } /// @@ -273,11 +274,13 @@ public static void RegisterInstance(string sessionID, object instance) /// The parameter names and types that will be positionally bound to the function; the last entry is the return type /// Optional default values for parameters /// A reference to the Function + [Obsolete("Use ServiceBinder.BindMethod; unlike RegisterFuction it throws when the name is already registered instead of replacing it.")] public void RegisterFuction(string methodName, Dictionary parameterNameTypeMapping, Dictionary parameterNameDefaultValueMapping, Delegate implementation) { MetaData.AddService(methodName, parameterNameTypeMapping, parameterNameDefaultValueMapping ?? new Dictionary(), implementation); } + [Obsolete("Use ServiceBinder.UnbindMethod.")] public void UnRegisterFunction(string methodName) { MetaData.RemoveService(methodName); diff --git a/Json-Rpc/Invocation/RpcMethod.cs b/Json-Rpc/Invocation/RpcMethod.cs index 36ad2c4..afc4a5f 100644 --- a/Json-Rpc/Invocation/RpcMethod.cs +++ b/Json-Rpc/Invocation/RpcMethod.cs @@ -100,10 +100,10 @@ public sealed partial class RpcMethod }; } - /// Compatibility overload preserving the original registration signature. - public static RpcMethod FromMethod(string name, MethodInfo method, object target, string[] parameterNames) + /// Compatibility overload preserving the original MethodInfo registration signature. + public static RpcMethod FromMethodInfo(string name, MethodInfo method, object target, string[] parameterNames) { - return FromMethod(name, method, target, parameterNames, RpcContextFlow.None); + return FromMethodInfo(name, method, target, parameterNames, RpcContextFlow.None); } /// Compatibility overload preserving the original delegate registration signature. @@ -112,8 +112,8 @@ public static RpcMethod FromDelegate(string name, Delegate implementation, strin return FromDelegate(name, implementation, parameterNames, defaults, RpcContextFlow.None); } - /// Builds the invokers for an instance (or static) method; are the JSON names (null = CLR names). - public static RpcMethod FromMethod(string name, MethodInfo method, object target, string[] parameterNames = null, RpcContextFlow contextFlow = RpcContextFlow.None) + /// Builds the invokers from a MethodInfo for an instance (or static) implementation; are the JSON names (null = CLR names). + public static RpcMethod FromMethodInfo(string name, MethodInfo method, object target, string[] parameterNames = null, RpcContextFlow contextFlow = RpcContextFlow.None) { RejectAsyncReturnType(name, method); var ps = method.GetParameters(); @@ -122,15 +122,15 @@ public static RpcMethod FromMethod(string name, MethodInfo method, object target } /// - /// Builds the invokers for an instance method whose receiver is produced per invocation. Right before the - /// method body runs, is called once with the RPC context of the request being + /// Builds the invokers from a MethodInfo for an instance implementation whose receiver is produced per invocation. Right before the + /// implementation body runs, is called once with the RPC context of the request being /// served (what returns) and must answer with an instance of /// . This is the seam for container-managed lifetimes: the resolver can look /// the request's scope up through the context and return a scoped or transient service. Nothing is cached - /// or disposed here. A static method keeps a null receiver and never resolves. A null result or another + /// or disposed here. A static implementation keeps a null receiver and never resolves. A null result or another /// type is an naming the service type, answered as -32603. /// - public static RpcMethod FromMethod(string name, MethodInfo method, Type serviceType, Func resolve, string[] parameterNames = null, RpcContextFlow contextFlow = RpcContextFlow.None) + public static RpcMethod FromMethodInfo(string name, MethodInfo method, Type serviceType, Func resolve, string[] parameterNames = null, RpcContextFlow contextFlow = RpcContextFlow.None) { if (method == null) throw new ArgumentNullException(nameof(method)); if (serviceType == null) throw new ArgumentNullException(nameof(serviceType)); @@ -139,7 +139,7 @@ public static RpcMethod FromMethod(string name, MethodInfo method, Type serviceT throw new ArgumentException("JSON-RPC method '" + name + "': the service type '" + serviceType + "' is not a closed type.", nameof(serviceType)); if (!method.DeclaringType.IsAssignableFrom(serviceType)) throw new ArgumentException("JSON-RPC method '" + name + "' is declared by '" + method.DeclaringType + "', which '" + serviceType + "' is not.", nameof(serviceType)); - if (method.IsStatic) return FromMethod(name, method, null, parameterNames, contextFlow); + if (method.IsStatic) return FromMethodInfo(name, method, null, parameterNames, contextFlow); RejectAsyncReturnType(name, method); var ps = method.GetParameters(); // (TService)ResolveReceiver(resolve, name): evaluated once per invocation. The arguments are read into diff --git a/Json-Rpc/JsonRpcService.cs b/Json-Rpc/JsonRpcService.cs index 9d5d8cf..c865794 100644 --- a/Json-Rpc/JsonRpcService.cs +++ b/Json-Rpc/JsonRpcService.cs @@ -21,10 +21,10 @@ protected JsonRpcService(bool autoBind) if (autoBind) ServiceBinder.BindService(Handler.DefaultSessionId(), this); } - /// Binds this instance to session , creating it when needed. - protected JsonRpcService(string sessionID) + /// Binds this instance to session , creating it when needed. + protected JsonRpcService(string sessionId) { - ServiceBinder.BindService(sessionID, this); + ServiceBinder.BindService(sessionId, this); } } } diff --git a/Json-Rpc/README.md b/Json-Rpc/README.md index 8212521..90e0de1 100644 --- a/Json-Rpc/README.md +++ b/Json-Rpc/README.md @@ -89,6 +89,8 @@ Both examples use the default session (`Handler.DefaultSessionId()`). Lambdas and classes can be mixed in one session when their method names differ; both examples register `add`, so keep one of them. The next step drives `CalculatorService` in process, without a transport. +A method is a callable identified by the `method` member of a request; its implementation is a delegate, a `[JsonRpcMethod]` member of a class, or a member of a bound interface. + ### Process requests Put this code in `Program.cs` in a console project targeting `net8.0` or `net10.0`. diff --git a/Json-Rpc/RpcInterfaceBindingOptions.cs b/Json-Rpc/RpcInterfaceBindingOptions.cs index 48086c2..897dfa3 100644 --- a/Json-Rpc/RpcInterfaceBindingOptions.cs +++ b/Json-Rpc/RpcInterfaceBindingOptions.cs @@ -45,17 +45,17 @@ public sealed class RpcInterfaceMethod internal RpcInterfaceMethod(MethodInfo method, string[] path, string leaf, string defaultName) { - Method = method; + MethodInfo = method; Interface = method.DeclaringType; _path = (string[])path.Clone(); Leaf = leaf; DefaultName = defaultName; } - /// The interface method declaration, including its parameter metadata and attributes. - public MethodInfo Method { get; } + /// The MethodInfo for the interface declaration, including its parameter metadata and attributes. + public MethodInfo MethodInfo { get; } - /// The closed interface declaring . + /// The closed interface declaring . public Type Interface { get; } /// A copy of the CLR property names from the root; empty for root methods. diff --git a/Json-Rpc/ServiceBinder.cs b/Json-Rpc/ServiceBinder.cs index f797161..c70d536 100644 --- a/Json-Rpc/ServiceBinder.cs +++ b/Json-Rpc/ServiceBinder.cs @@ -15,9 +15,9 @@ public static void BindMethod(string name, Delegate implementation, string[] par } /// Compatibility overload preserving the original session registration signature. - public static void BindMethod(string sessionID, string name, Delegate implementation, string[] parameterNames, IDictionary defaults) + public static void BindMethod(string sessionId, string name, Delegate implementation, string[] parameterNames, IDictionary defaults) { - BindMethod(sessionID, name, implementation, parameterNames, defaults, RpcContextFlow.None); + BindMethod(sessionId, name, implementation, parameterNames, defaults, RpcContextFlow.None); } /// Registers as method on the default session. See the session overload. @@ -28,7 +28,7 @@ public static void BindMethod(string name, Delegate implementation, string[] par /// /// Registers any delegate (a lambda, a method group, a closed instance method) as JSON-RPC method - /// on session , without attributes or a service class. + /// on session , without attributes or a service class. /// Parameters bind by the delegate's signature: positional params by order, named params by /// when given (null entries keep the lambda's own name), else by the /// lambda's parameter names, else arg1, arg2... for a delegate whose names are not recoverable. @@ -37,17 +37,17 @@ public static void BindMethod(string name, Delegate implementation, string[] par /// Task and ValueTask delegates require ProcessAsync; async void is rejected. /// controls ambient context across awaits. /// - public static void BindMethod(string sessionID, string name, Delegate implementation, string[] parameterNames = null, IDictionary defaults = null, RpcContextFlow contextFlow = RpcContextFlow.None) + public static void BindMethod(string sessionId, string name, Delegate implementation, string[] parameterNames = null, IDictionary defaults = null, RpcContextFlow contextFlow = RpcContextFlow.None) { - if (sessionID == null) throw new ArgumentNullException(nameof(sessionID)); + if (sessionId == null) throw new ArgumentNullException(nameof(sessionId)); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("A JSON-RPC method name is required.", nameof(name)); if (implementation == null) throw new ArgumentNullException(nameof(implementation)); var rpc = RpcMethod.FromDelegate(name, implementation, parameterNames, defaults, contextFlow); - var handler = Handler.GetSessionHandler(sessionID); + var handler = Handler.GetSessionHandler(sessionId); if (handler.MetaData.Services.ContainsKey(name)) { - throw new ArgumentException("JSON-RPC method '" + name + "' is already registered on session '" + sessionID + "'; unbind it first.", nameof(name)); + throw new ArgumentException("JSON-RPC method '" + name + "' is already registered on session '" + sessionId + "'; unbind it first.", nameof(name)); } var paras = new Dictionary(); @@ -65,10 +65,10 @@ public static void BindMethod(string sessionID, string name, Delegate implementa handler.MetaData.AddService(name, paras, defaultValues, implementation, rpc); } - /// Removes method from session ; false when it was not registered. - public static bool UnbindMethod(string sessionID, string name) + /// Removes method from session ; false when it was not registered. + public static bool UnbindMethod(string sessionId, string name) { - return Handler.GetSessionHandler(sessionID).MetaData.RemoveService(name); + return Handler.GetSessionHandler(sessionId).MetaData.RemoveService(name); } /// Removes method from the default session; false when it was not registered. @@ -81,24 +81,24 @@ public static bool UnbindMethod(string name) { BindService(Handler.DefaultSessionId()); } - public static void BindService(string sessionID) where T : new() + public static void BindService(string sessionId) where T : new() { - BindService(sessionID, new T()); + BindService(sessionId, new T()); } /// - /// Registers every [JsonRpcMethod] of 's type on session , + /// Registers every [JsonRpcMethod] of 's type on session , /// invoking them on that one instance from every thread; it must be thread-safe. /// - public static void BindService(string sessionID, Object instance) + public static void BindService(string sessionId, Object instance) { - if (sessionID == null) throw new ArgumentNullException(nameof(sessionID)); + if (sessionId == null) throw new ArgumentNullException(nameof(sessionId)); if (instance == null) throw new ArgumentNullException(nameof(instance)); - Bind(sessionID, instance.GetType(), instance, null); + Bind(sessionId, instance.GetType(), instance, null); } /// - /// Registers every [JsonRpcMethod] of on session + /// Registers every [JsonRpcMethod] of on session /// without an instance. Right before each call, is handed the RPC context of the /// request (what returns) and returns the instance to invoke; it runs once per /// invocation, on the invoking thread. This is how a container's scoped and transient lifetimes reach a method: @@ -107,18 +107,18 @@ public static void BindService(string sessionID, Object instance) /// methods never resolve. A resolver that returns null or another type fails the call with -32603 (an /// naming the service type, visible to the error handler). /// - public static void BindService(string sessionID, Type serviceType, Func resolve) + public static void BindService(string sessionId, Type serviceType, Func resolve) { - if (sessionID == null) throw new ArgumentNullException(nameof(sessionID)); + if (sessionId == null) throw new ArgumentNullException(nameof(sessionId)); if (serviceType == null) throw new ArgumentNullException(nameof(serviceType)); if (resolve == null) throw new ArgumentNullException(nameof(resolve)); if (serviceType.ContainsGenericParameters) throw new ArgumentException("A closed type is required: '" + serviceType + "'.", nameof(serviceType)); - Bind(sessionID, serviceType, null, resolve); + Bind(sessionId, serviceType, null, resolve); } /// Attribute discovery shared by the instance and the resolver overloads; exactly one of and is set. - private static void Bind(string sessionID, Type item, object instance, Func resolve) + private static void Bind(string sessionId, Type item, object instance, Func resolve) { var methods = item.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static) .Where(m => m.GetCustomAttributes(typeof(JsonRpcMethodAttribute), false).Length > 0); @@ -161,8 +161,8 @@ private static void Bind(string sessionID, Type item, object instance, Func l + r); @@ -212,7 +214,7 @@ binding.Dispose(); // unbinds this tree, leaving later replacements alone using var characterOnly = ServiceBinder.BindInterface(sessionId, world, new RpcInterfaceBindingOptions { Include = m => m.Path.Length == 1 }); -// Include can also inspect m.Method for the host's own interface attributes. +// Include can also inspect m.MethodInfo for the host's own interface attributes. ``` Each interface-typed property becomes a name segment, so `IWorld.Character.MoveAndRotate` is exposed as `Character.MoveAndRotate`. The whole tree is walked and compiled when you call `BindInterface`, so a request pays nothing for it. @@ -231,7 +233,7 @@ Naming, through `RpcInterfaceBindingOptions`: | `Prefix` | `""` | prepended to every generated name | | `Separator` | `"."` | joins property segments and the method name | | `Casing` | `Preserve` | `CamelCase` lower-cases the first letter of each generated segment (invariant culture) | -| `Include` | all | a predicate over `RpcInterfaceMethod` (`Path`, `Method`, `Interface`, `Leaf`, `DefaultName`) | +| `Include` | all | a predicate over `RpcInterfaceMethod` (`Path`, `MethodInfo`, `Interface`, `Leaf`, `DefaultName`) | | `NameRule` | none | returns the complete wire name, replacing the rules above | An explicit `[JsonRpcMethod("alias")]` on an interface method is used as written. `Task` and `ValueTask` members are served by `ProcessAsync`; `[JsonRpcMethod(ContextFlow = RpcContextFlow.Flow)]` on the interface member opts into context flow across awaits (see [Asynchronous methods and cancellation](#asynchronous-methods-and-cancellation)). @@ -412,7 +414,7 @@ The client gets a ticket immediately and polls, or the transport pushes a notifi ## Sessions and context -Sessions let you host independent sets of services (for example one per connected client or tenant): +A *session* is a named set of JSON-RPC methods with its own configuration, stored in a process-wide registry until explicitly destroyed; it has no connection lifetime of its own and no relationship to ASP.NET Core session state. Sessions let you host independent sets of methods, for example one per connected client or tenant: ```csharp ServiceBinder.BindService("client-42", new CalculatorService()); // any object with [JsonRpcMethod] members diff --git a/docs/upgrading.md b/docs/upgrading.md index 158866d..320ed05 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -4,6 +4,8 @@ Most 1.x services run unchanged. Read the first list before you build, and the s ## Changes that break the build +- **Session parameter.** The session parameter is spelled `sessionId` everywhere; a named argument `sessionID:` must be updated. +- **MethodInfo names.** `RpcMethod.FromMethod` is `FromMethodInfo` and `RpcInterfaceMethod.Method` is `MethodInfo` (both were new in the 2.0 preview). - **Serializer.** `JsonRpcProcessor.Process(…, JsonSerializerSettings)` is gone from the core. Use `Config.SetSerializer(new NewtonsoftJsonRpcSerializer(settings))` from the Newtonsoft package, or the helper overloads there that take the settings. - **Overloads.** The default-session string overloads that take a serializer take it first: `Process(serializer, json, context)` and `ProcessSync(serializer, json, context)`. `ProcessSync(sessionId, json, context, serializer)` makes `context` required, so `ProcessSync(json, null)` still means the default session. `Process` and `ProcessAsync` do not: `Process(json, null)` no longer compiles (it is ambiguous with the `JsonRpcStateAsync` overload), and `ProcessAsync(json, null)` binds to the session overload with `json` as the session id and a null document, which throws `ArgumentNullException`. Write `Process(json)`, `Process(json, context: null)` or `ProcessAsync(json, context: null)`. - **DTOs.** `JsonRequest`, `JsonResponse` and `JsonRpcException` are plain DTOs without Json.NET attributes. `JsonRequest.Params` is the active serializer's object model, so cast to `JObject`/`JArray` only when the Json.NET serializer is active.