Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions AustinHarris.JsonRpc.AspNetCore/JsonRpcOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ public class JsonRpcOptions
public string SessionId { get; set; }

/// <summary>
/// Picks the session per HTTP request (for example from a route value or a header). When set it takes
/// precedence over <see cref="SessionId"/>. 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 <see cref="SessionId"/>. Not used by the raw connection handler.
/// </summary>
public Func<HttpContext, string> SessionSelector { get; set; }

Expand Down
6 changes: 5 additions & 1 deletion AustinHarris.JsonRpcTestN/AsyncInvocationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ public async Task CompatibilityRegistrationSurfaces_SupportAsyncMethods(int surf
var method = new Func<Task<int>>(() => 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<string, object>(), 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);
Expand Down Expand Up @@ -172,13 +174,15 @@ public void AsyncVoid_IsRejectedOnEverySurface(int surface)
var types = new Dictionary<string, Type> { ["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<string, object>(), invalid),
#pragma warning disable CS0618
_ => () => Handler.GetSessionHandler(_session).RegisterFuction("invalid", types, null, invalid)
#pragma warning restore CS0618
};
StringAssert.Contains("async void", Assert.Throws<NotSupportedException>(registration).Message);
}
Expand Down
2 changes: 2 additions & 0 deletions AustinHarris.JsonRpcTestN/DelegateBindingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Type> { ["returns"] = typeof(int) }, null, new Func<int>(() => 3));
#pragma warning restore CS0618
Assert.AreEqual("{\"jsonrpc\":\"2.0\",\"result\":3,\"id\":1}", Run("{\"method\":\"m\",\"id\":1}"));
}

Expand Down
2 changes: 1 addition & 1 deletion AustinHarris.JsonRpcTestN/DiLifetimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ public void Arguments_AreChecked()
Assert.Throws<ArgumentNullException>(() => ServiceBinder.BindService(_session, (object)null));
Assert.Throws<ArgumentException>(() => ServiceBinder.BindService(_session, typeof(List<>), c => null));
var method = typeof(TaggedService).GetMethod(nameof(TaggedService.Tag));
var ex = Assert.Throws<ArgumentException>(() => AustinHarris.JsonRpc.Invocation.RpcMethod.FromMethod("fb.tag", method, typeof(string), c => null));
var ex = Assert.Throws<ArgumentException>(() => AustinHarris.JsonRpc.Invocation.RpcMethod.FromMethodInfo("fb.tag", method, typeof(string), c => null));
StringAssert.Contains(typeof(TaggedService).FullName, ex.Message);
}
}
Expand Down
2 changes: 2 additions & 0 deletions AustinHarris.JsonRpcTestN/DispatchHardeningTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion AustinHarris.JsonRpcTestN/InterfaceBindingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ public void Include_ReadsHostAttributeForEveryAlias()
var seen = new List<RpcInterfaceMethod>();
using var binding = ServiceBinder.BindInterface<IAliases>(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);
Expand Down
2 changes: 2 additions & 0 deletions AustinHarris.JsonRpcTestN/NewtonsoftTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Type> { { "s", typeof(string) }, { "returns", typeof(string) } }, null, new Func<string, string>(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);
Expand Down
2 changes: 2 additions & 0 deletions AustinHarris.JsonRpcTestN/SessionAndConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions AustinHarris.JsonRpcTestN/Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@
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<string, object>(),new Func<string, string>(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}";
Expand Down Expand Up @@ -132,7 +134,7 @@
public void NullableDateTimeToNullableDateTime()
{
string request = @"{""method"":""NullableDateTimeToNullableDateTime"",""params"":[""2014-06-30T14:50:38.5208399+09:00""],""id"":1}";
string expectedResult = "{\"jsonrpc\":\"2.0\",\"result\":\"2014-06-30T14:50:38.5208399+09:00\",\"id\":1}";

Check warning on line 137 in AustinHarris.JsonRpcTestN/Test.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

The variable 'expectedResult' is assigned but its value is never used

Check warning on line 137 in AustinHarris.JsonRpcTestN/Test.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

The variable 'expectedResult' is assigned but its value is never used

Check warning on line 137 in AustinHarris.JsonRpcTestN/Test.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

The variable 'expectedResult' is assigned but its value is never used

Check warning on line 137 in AustinHarris.JsonRpcTestN/Test.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

The variable 'expectedResult' is assigned but its value is never used
var expectedDate = DateTime.Parse("2014-06-30T14:50:38.5208399+09:00");
var result = JsonRpcProcessor.Process(request);
result.Wait();
Expand Down Expand Up @@ -1612,7 +1614,9 @@
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<string, object>(),new Func<string, string>(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}";
Expand Down Expand Up @@ -1852,7 +1856,9 @@
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<string, object>(), new Func<string, string>(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}";
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 7 additions & 4 deletions Json-Rpc/Handler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace AustinHarris.JsonRpc
using AustinHarris.JsonRpc.Serialization;
using System.Collections.Concurrent;

/// <summary>Dispatches requests for one session: a named set of JSON-RPC methods and configuration whose lifetime is managed explicitly.</summary>
public sealed partial class Handler
{
#region Members
Expand Down Expand Up @@ -112,7 +113,7 @@ public static Handler GetSessionHandler()
}

/// <summary>
/// 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
/// </summary>
public static void DestroySession(string sessionId)
{
Expand All @@ -135,7 +136,7 @@ public void Destroy()
public static Handler DefaultHandler { get { return GetSessionHandler(_defaultSessionId); } }

/// <summary>
/// The sessionID of this Handler
/// The sessionId of this Handler
/// </summary>
public string SessionId { get; private set; }

Expand Down Expand Up @@ -260,9 +261,9 @@ public static JsonRpcException RpcGetAndRemoveRpcException()
/// <summary>
/// Allows you to register all the functions on a Pojo Type that have been attributed as [JsonRpcMethod] to the specified sessionId
/// </summary>
public static void RegisterInstance(string sessionID, object instance)
public static void RegisterInstance(string sessionId, object instance)
{
ServiceBinder.BindService(sessionID, instance);
ServiceBinder.BindService(sessionId, instance);
}

/// <summary>
Expand All @@ -273,11 +274,13 @@ public static void RegisterInstance(string sessionID, object instance)
/// <param name="parameterNameTypeMapping">The parameter names and types that will be positionally bound to the function; the last entry is the return type</param>
/// <param name="parameterNameDefaultValueMapping">Optional default values for parameters</param>
/// <param name="implementation">A reference to the Function</param>
[Obsolete("Use ServiceBinder.BindMethod; unlike RegisterFuction it throws when the name is already registered instead of replacing it.")]
public void RegisterFuction(string methodName, Dictionary<string, Type> parameterNameTypeMapping, Dictionary<string, object> parameterNameDefaultValueMapping, Delegate implementation)
{
MetaData.AddService(methodName, parameterNameTypeMapping, parameterNameDefaultValueMapping ?? new Dictionary<string, object>(), implementation);
}

[Obsolete("Use ServiceBinder.UnbindMethod.")]
public void UnRegisterFunction(string methodName)
{
MetaData.RemoveService(methodName);
Expand Down
20 changes: 10 additions & 10 deletions Json-Rpc/Invocation/RpcMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ public sealed partial class RpcMethod
};
}

/// <summary>Compatibility overload preserving the original registration signature.</summary>
public static RpcMethod FromMethod(string name, MethodInfo method, object target, string[] parameterNames)
/// <summary>Compatibility overload preserving the original MethodInfo registration signature.</summary>
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);
}

/// <summary>Compatibility overload preserving the original delegate registration signature.</summary>
Expand All @@ -112,8 +112,8 @@ public static RpcMethod FromDelegate(string name, Delegate implementation, strin
return FromDelegate(name, implementation, parameterNames, defaults, RpcContextFlow.None);
}

/// <summary>Builds the invokers for an instance (or static) method; <paramref name="parameterNames"/> are the JSON names (null = CLR names).</summary>
public static RpcMethod FromMethod(string name, MethodInfo method, object target, string[] parameterNames = null, RpcContextFlow contextFlow = RpcContextFlow.None)
/// <summary>Builds the invokers from a MethodInfo for an instance (or static) implementation; <paramref name="parameterNames"/> are the JSON names (null = CLR names).</summary>
public static RpcMethod FromMethodInfo(string name, MethodInfo method, object target, string[] parameterNames = null, RpcContextFlow contextFlow = RpcContextFlow.None)
{
RejectAsyncReturnType(name, method);
var ps = method.GetParameters();
Expand All @@ -122,15 +122,15 @@ public static RpcMethod FromMethod(string name, MethodInfo method, object target
}

/// <summary>
/// Builds the invokers for an instance method whose receiver is produced per invocation. Right before the
/// method body runs, <paramref name="resolve"/> 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, <paramref name="resolve"/> is called once with the RPC context of the request being
/// served (what <see cref="Handler.RpcContext"/> returns) and must answer with an instance of
/// <paramref name="serviceType"/>. 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 <see cref="InvalidOperationException"/> naming the service type, answered as <c>-32603</c>.
/// </summary>
public static RpcMethod FromMethod(string name, MethodInfo method, Type serviceType, Func<object, object> resolve, string[] parameterNames = null, RpcContextFlow contextFlow = RpcContextFlow.None)
public static RpcMethod FromMethodInfo(string name, MethodInfo method, Type serviceType, Func<object, object> resolve, string[] parameterNames = null, RpcContextFlow contextFlow = RpcContextFlow.None)
{
if (method == null) throw new ArgumentNullException(nameof(method));
if (serviceType == null) throw new ArgumentNullException(nameof(serviceType));
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions Json-Rpc/JsonRpcService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ protected JsonRpcService(bool autoBind)
if (autoBind) ServiceBinder.BindService(Handler.DefaultSessionId(), this);
}

/// <summary>Binds this instance to session <paramref name="sessionID"/>, creating it when needed.</summary>
protected JsonRpcService(string sessionID)
/// <summary>Binds this instance to session <paramref name="sessionId"/>, creating it when needed.</summary>
protected JsonRpcService(string sessionId)
{
ServiceBinder.BindService(sessionID, this);
ServiceBinder.BindService(sessionId, this);
}
}
}
2 changes: 2 additions & 0 deletions Json-Rpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
8 changes: 4 additions & 4 deletions Json-Rpc/RpcInterfaceBindingOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <summary>The interface method declaration, including its parameter metadata and attributes.</summary>
public MethodInfo Method { get; }
/// <summary>The MethodInfo for the interface declaration, including its parameter metadata and attributes.</summary>
public MethodInfo MethodInfo { get; }

/// <summary>The closed interface declaring <see cref="Method"/>.</summary>
/// <summary>The closed interface declaring <see cref="MethodInfo"/>.</summary>
public Type Interface { get; }

/// <summary>A copy of the CLR property names from the root; empty for root methods.</summary>
Expand Down
Loading
Loading