Skip to content

Commit

Permalink
#16 Allow inheriting [JsonRpcMethod] from interface to implementing c…
Browse files Browse the repository at this point in the history
…lass.
  • Loading branch information
CXuesong committed Aug 16, 2020
1 parent 1bad715 commit 9800ebe
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 27 deletions.
121 changes: 103 additions & 18 deletions JsonRpc.Commons/Contracts/JsonRpcContractResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using JsonRpc.Server;

Expand Down Expand Up @@ -102,32 +103,37 @@ public JsonRpcClientContract CreateClientContract(IEnumerable<Type> contractType
protected virtual IEnumerable<KeyValuePair<MethodInfo, JsonRpcMethod>> MethodsFromType(Type serviceType)
{
if (serviceType == null) throw new ArgumentNullException(nameof(serviceType));
var scope = serviceType.GetTypeInfo().GetCustomAttribute<JsonRpcScopeAttribute>();
return serviceType.GetRuntimeMethods()
.Where(m => m.GetCustomAttribute<JsonRpcMethodAttribute>() != null)
.Select(m => new KeyValuePair<MethodInfo, JsonRpcMethod>(m, CreateMethod(serviceType, m, scope)));
.Select(m => new KeyValuePair<MethodInfo, JsonRpcMethod>(m, CreateMethod(serviceType, m)))
.Where(p => p.Value != null);
}

protected virtual JsonRpcMethod CreateMethod(Type serviceType, MethodInfo method, JsonRpcScopeAttribute scopeAttribute)
protected virtual JsonRpcMethod CreateMethod(Type serviceType, MethodInfo method)
{
if (serviceType == null) throw new ArgumentNullException(nameof(serviceType));
if (method == null) throw new ArgumentNullException(nameof(method));
var inst = new JsonRpcMethod { ServiceType = serviceType };
var attr = method.GetCustomAttribute<JsonRpcMethodAttribute>();
inst.MethodName = attr?.MethodName;
if (attr?.MethodName == null)
inst.MethodName = NamingStrategy.GetRpcMethodName(method.Name, false);
else
inst.MethodName = NamingStrategy.GetRpcMethodName(attr.MethodName, true);
if (scopeAttribute?.MethodPrefix != null)
inst.MethodName = scopeAttribute.MethodPrefix + inst.MethodName;
inst.IsNotification = attr?.IsNotification ?? false;
inst.AllowExtensionData = attr?.AllowExtensionData ?? false;
inst.ReturnParameter = CreateParameter(serviceType, method.ReturnParameter, attr, scopeAttribute);
var attr = CachedAttributeAccessor<JsonRpcMethodAttribute>.Get(method);
if (attr == null) return null;

Debug.Assert(serviceType.GetTypeInfo().IsAssignableFrom(method.DeclaringType.GetTypeInfo()));
var inst = new JsonRpcMethod
{
ServiceType = serviceType,
MethodName = attr.MethodName == null
? NamingStrategy.GetRpcMethodName(method.Name, false)
: NamingStrategy.GetRpcMethodName(attr.MethodName, true)
};

var scope = CachedAttributeAccessor<JsonRpcScopeAttribute>.Get(serviceType);
if (scope?.MethodPrefix != null)
inst.MethodName = scope.MethodPrefix + inst.MethodName;
inst.IsNotification = attr.IsNotification;
inst.AllowExtensionData = attr.AllowExtensionData;
inst.ReturnParameter = CreateParameter(serviceType, method.ReturnParameter, attr, scope);
// Even void-type method has its ReturnParameter.
Debug.Assert(inst.ReturnParameter != null, "CreateParameter should not return null.");
inst.Parameters = method.GetParameters()
.Select(p => CreateParameter(serviceType, p, attr, scopeAttribute))
.Select(p => CreateParameter(serviceType, p, attr, scope))
.ToList();
Debug.Assert(inst.Parameters.IndexOf(null) < 0, "CreateParameter should not return null.");
inst.Invoker = new ReflectionJsonRpcMethodInvoker(serviceType, method);
Expand Down Expand Up @@ -156,7 +162,7 @@ protected virtual JsonRpcParameter CreateParameter(Type serviceType, ParameterIn
if (!isReturnParam && taskResultType != null)
throw new NotSupportedException("Argument with type of System.Threading.Task is not supported.");
// TODO bypass return value attribute check ONLY on Mono with FrameworkDescription check.
var attr = isReturnParam ? null : parameter.GetCustomAttribute<JsonRpcParameterAttribute>();
var attr = isReturnParam ? null : CachedAttributeAccessor<JsonRpcParameterAttribute>.Get(parameter);
var inst = new JsonRpcParameter
{
IsOptional = attr?.IsOptional ?? parameter.IsOptional,
Expand Down Expand Up @@ -187,5 +193,84 @@ protected virtual JsonRpcParameter CreateParameter(Type serviceType, ParameterIn
if (inst.ParameterType == typeof(CancellationToken)) inst.IsOptional = true;
return inst;
}

private static class CachedAttributeAccessor<T> where T : Attribute
{

private static readonly ConditionalWeakTable<object, T> cache = new ConditionalWeakTable<object, T>();

#if !BCL_FEATURE_TYPE_IS_MEMBER_INFO
public static T Get(Type type) => Get(type.GetTypeInfo());
#endif

private static T GetDirect(MemberInfo memberInfo)
{
var attr = memberInfo.GetCustomAttribute<T>();
if (attr != null) return attr;
if (memberInfo.DeclaringType == null) return null;

// If we cannot find attribute on the class method, we check whether this method is implementing an interface with [T].
var ownerType = memberInfo.DeclaringType;
if (ownerType == null) return null;
#if BCL_FEATURE_TYPE_IS_MEMBER_INFO
foreach (var interfaceType in ownerType.GetInterfaces())
{
var map = ownerType.GetInterfaceMap(interfaceType);
#else
foreach (var interfaceType in ownerType.GetTypeInfo().ImplementedInterfaces)
{
var map = ownerType.GetTypeInfo().GetRuntimeInterfaceMap(interfaceType);
#endif
var index = Array.IndexOf(map.TargetMethods, memberInfo);
if (index >= 0)
{
var interfaceMethod = map.InterfaceMethods[index];
return interfaceMethod.GetCustomAttribute<T>();
}
}
return null;
}

public static T Get(MemberInfo memberInfo)
{
if (cache.TryGetValue(memberInfo, out var attr))
return attr;
attr = GetDirect(memberInfo);
#if BCL_FEATURE_CWT_ADD_OR_UPDATE
cache.AddOrUpdate(memberInfo, attr);
#else
try
{
cache.Add(memberInfo, attr);
}
catch (ArgumentException)
{
// Conflicting key.
}
#endif
return attr;
}

public static T Get(ParameterInfo paramInfo)
{
if (cache.TryGetValue(paramInfo, out var attr))
return attr;
attr = paramInfo.GetCustomAttribute<T>();
#if BCL_FEATURE_CWT_ADD_OR_UPDATE
cache.AddOrUpdate(paramInfo, attr);
#else
try
{
cache.Add(paramInfo, attr);
}
catch (ArgumentException)
{
// Conflicting key.
}
#endif
return attr;
}

}
}
}
8 changes: 2 additions & 6 deletions Shared/JsonRpc.Commons.props
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
<Project>
<PropertyGroup Condition=" $([System.Text.RegularExpressions.Regex]::IsMatch($(TargetFramework), `^netcoreapp(2.1|3.0|3.1|5.0)|netstandard2.[1-9]$`)) " >
<DefineConstants>$(DefineConstants);BCL_FEATURE_READER_WRITER_CANCEL</DefineConstants>
<DefineConstants>$(DefineConstants);BCL_FEATURE_READER_WRITER_CANCEL;BCL_FEATURE_ASYNC_ENUMERABLE;BCL_FEATURE_CWT_ADD_OR_UPDATE</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition=" $([System.Text.RegularExpressions.Regex]::IsMatch($(TargetFramework), `^netcoreapp(2.1|3.0|3.1|5.0)|netstandard2.[1-9]$`)) ">
<DefineConstants>$(DefineConstants);BCL_FEATURE_ASYNC_ENUMERABLE</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition=" $([System.Text.RegularExpressions.Regex]::IsMatch($(TargetFramework), `^netcoreapp(3.\d|5.\d)|netstandard2.[1-9]$`)) ">
<DefineConstants>$(DefineConstants);BCL_FEATURE_ASYNC_DISPOSABLE</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition=" $([System.Text.RegularExpressions.Regex]::IsMatch($(TargetFramework), `^netcoreapp(2.0|2.1|3.0|3.1|5.0)|netstandard(2.\d)|net\d+$`)) ">
<DefineConstants>$(DefineConstants);BCL_FEATURE_SERIALIZATION</DefineConstants>
<DefineConstants>$(DefineConstants);BCL_FEATURE_SERIALIZATION;BCL_FEATURE_TYPE_IS_MEMBER_INFO</DefineConstants>
</PropertyGroup>
</Project>
14 changes: 11 additions & 3 deletions UnitTestProject1/Helpers/TestJsonRpcService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,17 @@ public interface ITestRpcCancellationContract

}

public class TestJsonRpcService : JsonRpcService
// For testing deriving [JsonRpcMethod] from implementing interface in TestJsonRpcService.
public interface ITestJsonRpcServiceAddPartial
{
[JsonRpcMethod]
int Add(int x, int y, CancellationToken ct);

[JsonRpcMethod("add")]
Task<string> AddAsync(string a, string b);
}

public class TestJsonRpcService : JsonRpcService, ITestJsonRpcServiceAddPartial
{
[JsonRpcMethod]
public int One()
Expand All @@ -133,13 +143,11 @@ public int AddMany([JsonRpcParameter(DefaultValue = new[] {1, 2, 3})] int[] valu
return values.Sum();
}

[JsonRpcMethod]
public int Add(int x, int y, CancellationToken ct)
{
return x + y;
}

[JsonRpcMethod("add")]
public async Task<string> AddAsync(string a, string b)
{
// do some work asynchronously…
Expand Down

0 comments on commit 9800ebe

Please sign in to comment.