diff --git a/src/EdjCase.JsonRpc.Router/BuilderExtensions.cs b/src/EdjCase.JsonRpc.Router/BuilderExtensions.cs
index ecea850..0a5edef 100644
--- a/src/EdjCase.JsonRpc.Router/BuilderExtensions.cs
+++ b/src/EdjCase.JsonRpc.Router/BuilderExtensions.cs
@@ -37,7 +37,10 @@ public static IServiceCollection AddJsonRpc(this IServiceCollection serviceColle
/// IoC serivce container to register JsonRpc dependencies
/// (Optional) Server wide rpc configuration
/// IoC service container
- public static IServiceCollection AddJsonRpc(this IServiceCollection serviceCollection, RpcServerConfiguration? configuration = null)
+ public static IServiceCollection AddJsonRpc(
+ this IServiceCollection serviceCollection,
+ RpcServerConfiguration? configuration = null
+ )
{
if (serviceCollection == null)
{
@@ -62,7 +65,11 @@ public static IServiceCollection AddJsonRpc(this IServiceCollection serviceColle
serviceCollection
.TryAddScoped();
serviceCollection
- .TryAddScoped();
+ .TryAddScoped();
+ serviceCollection
+ .TryAddSingleton();
+ serviceCollection
+ .AddOptions();
serviceCollection
.TryAddScoped();
serviceCollection
diff --git a/src/EdjCase.JsonRpc.Router/Defaults/DefaultRequestMatcher.cs b/src/EdjCase.JsonRpc.Router/Defaults/DefaultRequestMatcher.cs
index 80f724e..ed89ec2 100644
--- a/src/EdjCase.JsonRpc.Router/Defaults/DefaultRequestMatcher.cs
+++ b/src/EdjCase.JsonRpc.Router/Defaults/DefaultRequestMatcher.cs
@@ -1,10 +1,8 @@
using System;
using System.Buffers;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
-using EdjCase.JsonRpc.Router;
using EdjCase.JsonRpc.Common;
using EdjCase.JsonRpc.Router.Abstractions;
using EdjCase.JsonRpc.Router.Utilities;
@@ -13,92 +11,74 @@
using Microsoft.Extensions.Caching.Memory;
namespace EdjCase.JsonRpc.Router.Defaults
-{
+{
+
internal class DefaultRequestMatcher : IRpcRequestMatcher
{
- private static ConcurrentDictionary> requestToMethodCache { get; }
- = new ConcurrentDictionary>();
-
private ILogger logger { get; }
private IRpcMethodProvider methodProvider { get; }
private IRpcContextAccessor contextAccessor { get; }
- private IRpcParameterConverter rpcParameterConverter { get; }
-
- public DefaultRequestMatcher(ILogger logger,
- IRpcMethodProvider methodProvider,
- IRpcContextAccessor contextAccessor,
- IRpcParameterConverter rpcParameterConverter)
- {
- this.contextAccessor = contextAccessor;
- this.logger = logger;
- this.methodProvider = methodProvider;
- this.contextAccessor = contextAccessor;
- this.rpcParameterConverter = rpcParameterConverter;
- }
-
- public IRpcMethodInfo GetMatchingMethod(RpcRequestSignature requestSignature)
- {
- this.logger.AttemptingToMatchMethod(new string(requestSignature.GetMethodName().Span));
-
- RpcContext context = this.contextAccessor.Get();
- IReadOnlyList? methods = this.methodProvider.GetByPath(context.Path);
- if (methods == null || !methods.Any())
- {
- throw new RpcException(RpcErrorCode.MethodNotFound, $"No methods found for route");
- }
-
- Span matches = this.FilterAndBuildMethodInfoByRequest(context, methods, requestSignature);
- if (matches.Length == 1)
- {
- this.logger.RequestMatchedMethod();
- return matches[0];
- }
-
- string errorMessage;
- if (matches.Length > 1)
- {
- var methodInfoList = new List();
- foreach (IRpcMethodInfo matchedMethod in matches)
- {
- var parameterTypeList = new List();
- foreach (IRpcParameterInfo parameterInfo in matchedMethod.Parameters)
- {
- RpcParameterType type = this.rpcParameterConverter.GetRpcParameterType(parameterInfo.RawType);
- string parameterType = parameterInfo.Name + ": " + type;
- if (parameterInfo.IsOptional)
- {
- parameterType += "(Optional)";
- }
- parameterTypeList.Add(parameterType);
- }
- string parameterString = string.Join(", ", parameterTypeList);
- methodInfoList.Add($"{{Name: '{matchedMethod.Name}', Parameters: [{parameterString}]}}");
- }
- errorMessage = "More than one method matched the rpc request. Unable to invoke due to ambiguity. Methods that matched the same name: " + string.Join(", ", methodInfoList);
- }
- else
- {
- //Log diagnostics
- this.logger.MethodsInRoute(methods);
- errorMessage = "No methods matched request.";
- }
- throw new RpcException(RpcErrorCode.MethodNotFound, errorMessage);
+ private IRpcParameterConverter rpcParameterConverter { get; }
+ private RequestMatcherCache requestMatcherCache { get; }
+ public DefaultRequestMatcher(
+ ILogger logger,
+ IRpcMethodProvider methodProvider,
+ IRpcContextAccessor contextAccessor,
+ IRpcParameterConverter rpcParameterConverter,
+ RequestMatcherCache requestMatcherCache
+ )
+ {
+ this.contextAccessor = contextAccessor;
+ this.logger = logger;
+ this.methodProvider = methodProvider;
+ this.rpcParameterConverter = rpcParameterConverter;
+ this.requestMatcherCache = requestMatcherCache;
+ }
+
+
+ public IRpcMethodInfo GetMatchingMethod(RpcRequestSignature requestSignature)
+ {
+ this.logger.AttemptingToMatchMethod(new string(requestSignature.GetMethodName().Span));
+
+ // Create efficient cache key from path and signature
+ string path = this.contextAccessor.Get().Path?.ToString() ?? string.Empty;
+
+ IRpcMethodInfo[] matchingMethods = this.requestMatcherCache.GetOrAdd(
+ path,
+ requestSignature,
+ () =>
+ {
+ RpcContext context = this.contextAccessor.Get();
+ IReadOnlyList? methods = this.methodProvider.GetByPath(context.Path);
+ if (methods == null || !methods.Any())
+ {
+ return Array.Empty();
+ }
+
+ return this.GetMatchingMethods(requestSignature, methods);
+ });
+
+
+ return this.HandleMatchResult(matchingMethods);
+ }
+
+ private IRpcMethodInfo HandleMatchResult(IRpcMethodInfo[] matches)
+ {
+ if (matches.Length == 1)
+ {
+ this.logger.RequestMatchedMethod();
+ return matches[0];
+ }
+ if (matches.Length > 1)
+ {
+ // Format the methods for error message
+ string errorMessage = "More than one method matched the rpc request. Unable to invoke due to ambiguity.";
+ throw new RpcException(RpcErrorCode.MethodNotFound, errorMessage);
+ }
+
+ throw new RpcException(RpcErrorCode.MethodNotFound, "No methods matched request.");
}
- private IRpcMethodInfo[] FilterAndBuildMethodInfoByRequest(RpcContext context, IReadOnlyList methods, RpcRequestSignature requestSignature)
- {
- //If the request signature is found, it means we have the methods cached already
-
- string rpcPath = context.Path?.ToString() ?? string.Empty;
- var rpcPathMethodsCache = DefaultRequestMatcher.requestToMethodCache.GetOrAdd(rpcPath, path => new ConcurrentDictionary());
- return rpcPathMethodsCache.GetOrAdd(requestSignature, BuildMethodCache);
- IRpcMethodInfo[] BuildMethodCache(RpcRequestSignature s)
- {
- return this.GetMatchingMethods(s, methods);
- }
- }
-
-
private IRpcMethodInfo[] GetMatchingMethods(RpcRequestSignature requestSignature, IReadOnlyList methods)
{
IRpcMethodInfo[] methodsWithSameName = ArrayPool.Shared.Rent(methods.Count);
@@ -260,7 +240,83 @@ private bool ParametersMatch(RpcRequestSignature requestSignature, IReadOnlyList
}
- }
+ }
+
+
+ public class RequestCacheOptions
+ {
+ public long SizeLimit { get; set; } = 100;
+ public TimeSpan? SlidingExpiration { get; set; } = null;
+ public TimeSpan? AbsoluteExpiration { get; set; } = null;
+ }
+
+ internal class RequestMatcherCache
+ {
+ private RequestCacheOptions options { get; }
+ private MemoryCache memoryCache { get; }
+
+ public RequestMatcherCache(IOptions options)
+ {
+ this.options = options.Value;
+ this.memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions
+ {
+ SizeLimit = this.options.SizeLimit
+ }));
+ }
+
+ public IRpcMethodInfo[] GetOrAdd(
+ string path,
+ RpcRequestSignature requestSignature,
+ Func resolveFunc
+ )
+ {
+ var cacheKey = new RequestCacheKey(path, requestSignature);
+
+ if (this.memoryCache.TryGetValue(cacheKey, out IRpcMethodInfo[]? result))
+ {
+ return result!;
+ }
+
+ IRpcMethodInfo[] matchingMethods = resolveFunc();
+
+ if (matchingMethods.Length != 1)
+ {
+ return matchingMethods; // Don't cache bad matches
+ }
+
+ // Cache with configurable options
+ var cacheEntryOptions = new MemoryCacheEntryOptions()
+ .SetSize(1);
+ if (this.options.SlidingExpiration != null)
+ {
+ cacheEntryOptions = cacheEntryOptions.SetSlidingExpiration(this.options.SlidingExpiration.Value);
+ }
+ if (this.options.AbsoluteExpiration != null)
+ {
+ cacheEntryOptions = cacheEntryOptions.SetAbsoluteExpiration(this.options.AbsoluteExpiration.Value);
+ }
+ return this.memoryCache.Set(cacheKey, matchingMethods, cacheEntryOptions);
+ }
+ }
+
+ internal readonly struct RequestCacheKey : IEquatable
+ {
+ private readonly string path;
+ private readonly string signatureKey;
+
+ public RequestCacheKey(string path, RpcRequestSignature signature)
+ {
+ this.path = path;
+ this.signatureKey = signature.AsString();
+ }
+
+ public bool Equals(RequestCacheKey other) => this.path == other.path && this.signatureKey == other.signatureKey;
+
+ public override bool Equals(object? obj) => obj is RequestCacheKey other && this.Equals(other);
+
+ public override int GetHashCode() => HashCode.Combine(this.path, this.signatureKey);
+ }
+
internal class RpcEndpointInfo
{
diff --git a/test/Benchmarks/Program.cs b/test/Benchmarks/Program.cs
index cf12e42..f6663c4 100644
--- a/test/Benchmarks/Program.cs
+++ b/test/Benchmarks/Program.cs
@@ -57,7 +57,13 @@ public void GlobalSetup()
JsonSerializerSettings = null
});
var rpcParameterConverter = new DefaultRpcParameterConverter(options, new FakeLogger());
- this.requestMatcher = new DefaultRequestMatcher(logger, methodProvider, fakeRpcContextAccessor, rpcParameterConverter);
+ this.requestMatcher = new DefaultRequestMatcher(
+ logger,
+ methodProvider,
+ fakeRpcContextAccessor,
+ rpcParameterConverter,
+ new RequestMatcherCache(Options.Create(new RequestCacheOptions()))
+ );
}
private RpcRequestSignature? requestsignature;
diff --git a/test/EdjCase.JsonRpc.Router.Sample/Startup.cs b/test/EdjCase.JsonRpc.Router.Sample/Startup.cs
index d613197..117faea 100644
--- a/test/EdjCase.JsonRpc.Router.Sample/Startup.cs
+++ b/test/EdjCase.JsonRpc.Router.Sample/Startup.cs
@@ -64,7 +64,13 @@ void ConfigureRpc(RpcServerConfiguration config)
services
.AddControllers()
.Services
- .AddJsonRpcWithSwagger(ConfigureRpc, globalJsonSerializerOptions);
+ .AddJsonRpcWithSwagger(ConfigureRpc, globalJsonSerializerOptions)
+ .Configure(options =>
+ {
+ options.SizeLimit = 10;
+ options.SlidingExpiration = TimeSpan.FromMinutes(5);
+ options.AbsoluteExpiration = TimeSpan.FromMinutes(90);
+ });
}
diff --git a/test/EdjCase.JsonRpc.Router.Tests/MethodMatcherTests.cs b/test/EdjCase.JsonRpc.Router.Tests/MethodMatcherTests.cs
index a2f1eb7..63455dd 100644
--- a/test/EdjCase.JsonRpc.Router.Tests/MethodMatcherTests.cs
+++ b/test/EdjCase.JsonRpc.Router.Tests/MethodMatcherTests.cs
@@ -22,23 +22,23 @@ public class MethodMatcherTests
private readonly StaticRpcMethodDataAccessor methodDataAccessor;
public MethodMatcherTests()
- {
+ {
var baserouteData = typeof(MethodMatcherThreeController)
.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)
.Select(DefaultRpcMethodInfo.FromMethodInfo)
- .ToList();
-
- var routeData = new Dictionary>
- {
+ .ToList();
+
+ var routeData = new Dictionary>
+ {
[nameof(MethodMatcherController)] = typeof(MethodMatcherController)
.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)
.Select(DefaultRpcMethodInfo.FromMethodInfo)
- .ToList(),
+ .ToList(),
[nameof(MethodMatcherDuplicatesController)] = typeof(MethodMatcherDuplicatesController)
.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)
.Select(DefaultRpcMethodInfo.FromMethodInfo)
- .ToList()
- };
+ .ToList()
+ };
this.methodDataAccessor = new StaticRpcMethodDataAccessor() { Value = new RpcRouteMetaData(baserouteData, routeData)};
}
@@ -64,7 +64,13 @@ private DefaultRequestMatcher GetMatcher(RpcPath? path = null)
var methodProvider = new StaticRpcMethodProvider(this.methodDataAccessor);
- return new DefaultRequestMatcher(logger.Object, methodProvider, rpcContextAccessor.Object, rpcParameterConverter);
+ return new DefaultRequestMatcher(
+ logger.Object,
+ methodProvider,
+ rpcContextAccessor.Object,
+ rpcParameterConverter,
+ new RequestMatcherCache(Options.Create(new RequestCacheOptions()))
+ );
}
[Fact]
@@ -81,8 +87,8 @@ public void GetMatchingMethod_WithRpcRoute()
DefaultRequestMatcher path2Matcher = this.GetMatcher(path: typeof(MethodMatcherDuplicatesController).GetTypeInfo().Name);
IRpcMethodInfo path2Match = path2Matcher.GetMatchingMethod(requestSignature);
Assert.NotNull(path2Match);
- Assert.NotSame(path1Match, path2Match);
-
+ Assert.NotSame(path1Match, path2Match);
+
DefaultRequestMatcher path3Matcher = this.GetMatcher(path: null);
IRpcMethodInfo path3Match = path3Matcher.GetMatchingMethod(requestSignature);
Assert.NotNull(path2Match);
@@ -352,15 +358,15 @@ public void GetMatchingMethod__Dictionary_Request_With_Optional_Parameters__Matc
Assert.NotNull(methodInfo);
Assert.Equal(methodName, methodInfo.Name);
- }
-
-
+ }
+
+
[Fact]
public void GetMatchingMethod_WithoutRpcRoute()
{
string methodName = nameof(MethodMatcherController.GuidTypeMethod);
RpcRequestSignature requestSignature = RpcRequestSignature.Create(methodName, new[] { RpcParameterType.String });
-
+
DefaultRequestMatcher path3Matcher = this.GetMatcher(path: null);
IRpcMethodInfo path3Match = path3Matcher.GetMatchingMethod(requestSignature);
Assert.NotNull(path3Match);