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