Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for deserializing params *object* as single first parameter #347

Merged
merged 9 commits into from
Nov 4, 2019
139 changes: 136 additions & 3 deletions src/StreamJsonRpc.Tests/JsonRpcTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ private interface IServer

[JsonRpcMethod("IFaceNameForMethod")]
int AddWithNameSubstitution(int a, int b);

[JsonRpcMethod(UseSingleObjectParameterDeserialization = true)]
int InstanceMethodWithSingleObjectParameterAndCancellationToken(XAndYFields fields, CancellationToken token);
}

[Fact]
Expand Down Expand Up @@ -942,11 +945,67 @@ public async Task InvokeWithParameterObject_ProgressAndDefaultParameters()
}

[SkippableFact]
public async Task InvokeWithParameterObject_ClassIncludingProgressProperty()
public async Task InvokeWithSingleObjectParameter_SendingExpectedObject()
{
Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack");

int sum = await this.clientRpc.InvokeWithParameterObjectAsync<int>("test/MethodWithSingleObjectParameter", new XAndYFields { x = 2, y = 5 }, this.TimeoutToken);
Assert.Equal(7, sum);
}

[Fact]
public async Task InvokeWithSingleObjectParameter_ServerMethodExpectsObjectButDoesNotSetDeserializationProperty()
{
await Assert.ThrowsAsync<RemoteMethodNotFoundException>(async () => await this.clientRpc.InvokeWithParameterObjectAsync<int>(nameof(Server.MethodWithSingleObjectParameterWithoutDeserializationProperty), new XAndYFields { x = 2, y = 5 }, this.TimeoutToken));
}

[SkippableFact]
public async Task InvokeWithSingleObjectParameter_ServerMethodExpectsObjectButSendingDifferentType()
{
Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack");

int sum = await this.clientRpc.InvokeWithParameterObjectAsync<int>("test/MethodWithSingleObjectParameterVAndW", new XAndYFields { x = 2, y = 5 }, this.TimeoutToken);

Assert.Equal(0, sum);
}

[Fact]
public async Task InvokeWithSingleObjectParameter_ServerMethodSetDeserializationPropertyButExpectMoreThanOneParameter()
{
await Assert.ThrowsAsync<RemoteMethodNotFoundException>(async () => await this.clientRpc.InvokeWithParameterObjectAsync<int>("test/MethodWithObjectAndExtraParameters", new XAndYFields { x = 2, y = 5 }, this.TimeoutToken));
}

[SkippableFact]
public async Task InvokeWithSingleObjectParameter_SendingExpectedObjectAndCancellationToken()
{
Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack");

int sum = await this.clientRpc.InvokeWithParameterObjectAsync<int>(nameof(Server.MethodWithSingleObjectParameterAndCancellationToken), new XAndYFields { x = 2, y = 5 }, this.TimeoutToken);
Assert.Equal(7, sum);
}

[SkippableFact]
public async Task InvokeWithSingleObjectParameter_SendingExpectedObjectAndCancellationToken_InterfaceMethodAttributed()
{
Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack");

int sum = await this.clientRpc.InvokeWithParameterObjectAsync<int>(nameof(IServer.InstanceMethodWithSingleObjectParameterAndCancellationToken), new XAndYFields { x = 2, y = 5 }, this.TimeoutToken);
Assert.Equal(7, sum);
}

[SkippableFact]
public async Task InvokeWithSingleObjectParameter_SendingWithProgressProperty()
{
Skip.If(this.clientMessageFormatter is MessagePackFormatter, "IProgress<T> serialization is not supported for MessagePack");

int sum = await this.clientRpc.InvokeWithParameterObjectAsync<int>(nameof(Server.MethodWithProgressAndMoreParameters), new XAndYFieldsWithProgress { x = 2, y = 5, p = new Progress<int>() }, this.TimeoutToken);
int report = 0;
var progress = new ProgressWithCompletion<int>(n => report += n);

int sum = await this.clientRpc.InvokeWithParameterObjectAsync<int>("test/MethodWithSingleObjectParameterWithProgress", new XAndYFieldsWithProgress { x = 2, y = 5, p = progress }, this.TimeoutToken);

await progress.WaitAsync();

Assert.Equal(7, report);
Assert.Equal(7, sum);
}

Expand Down Expand Up @@ -1259,6 +1318,22 @@ public async Task AddLocalRpcMethod_String_MethodInfo_Object_NullTargetForStatic
Assert.Equal("foo!", result);
}

[Fact]
public async Task AddLocalRpcMethod_MethodInfo_Object_Attribute()
{
this.ReinitializeRpcWithoutListening();

MethodInfo methodInfo = typeof(Server).GetTypeInfo().DeclaredMethods.Single(m => m.Name == nameof(Server.ServerMethod));
Assumes.True(methodInfo.IsStatic); // we picked this method because it's static.
this.serverRpc.AddLocalRpcMethod(methodInfo, null, new JsonRpcMethodAttribute("biz.bar"));

this.serverRpc.StartListening();
this.clientRpc.StartListening();

string result = await this.clientRpc.InvokeAsync<string>("biz.bar", "foo");
Assert.Equal("foo!", result);
}

[Fact]
public void AddLocalRpcMethod_String_MethodInfo_Object_NonNullTargetForStaticMethod()
{
Expand Down Expand Up @@ -1657,6 +1732,47 @@ public static int MethodWithDefaultParameter(int x, int y = 10)
return x + y;
}

public static int MethodWithOneNonObjectParameter(int x)
{
return x;
}

[JsonRpcMethod("test/MethodWithSingleObjectParameter", UseSingleObjectParameterDeserialization = true)]
public static int MethodWithSingleObjectParameter(XAndYFields fields)
{
return fields.x + fields.y;
}

public static int MethodWithSingleObjectParameterWithoutDeserializationProperty(XAndYFields fields)
{
return fields.x + fields.y;
}

[JsonRpcMethod("test/MethodWithSingleObjectParameterVAndW", UseSingleObjectParameterDeserialization = true)]
public static int MethodWithSingleObjectParameterVAndW(VAndWFields fields)
{
return fields.v + fields.w;
}

[JsonRpcMethod(UseSingleObjectParameterDeserialization = true)]
public static int MethodWithSingleObjectParameterAndCancellationToken(XAndYFields fields, CancellationToken token)
{
return fields.x + fields.y;
}

[JsonRpcMethod("test/MethodWithSingleObjectParameterWithProgress", UseSingleObjectParameterDeserialization = true)]
public static int MethodWithSingleObjectParameterWithProgress(XAndYFieldsWithProgress fields)
{
fields.p.Report(fields.x + fields.y);
return fields.x + fields.y;
}

[JsonRpcMethod("test/MethodWithObjectAndExtraParameters", UseSingleObjectParameterDeserialization = true)]
public static int MethodWithObjectAndExtraParameters(XAndYFields fields, int anotherParameter)
{
return fields.x + fields.y + anotherParameter;
}
AArnott marked this conversation as resolved.
Show resolved Hide resolved

public static int MethodWithProgressParameter(IProgress<int> p)
{
p.Report(1);
Expand Down Expand Up @@ -1689,6 +1805,11 @@ public static int MethodWithInvalidProgressParameter(Progress<int> p)
return 1;
}

public int InstanceMethodWithSingleObjectParameterAndCancellationToken(XAndYFields fields, CancellationToken token)
{
return fields.x + fields.y;
}

public int? MethodReturnsNullableInt(int a) => a > 0 ? (int?)a : null;

public int MethodAcceptsNullableArgs(int? a, int? b) => (a.HasValue ? 1 : 0) + (b.HasValue ? 1 : 0);
Expand Down Expand Up @@ -1937,7 +2058,7 @@ internal void InternalMethod()
this.ServerMethodReached.Set();
}

[JsonRpcMethod("InternalMethodWithAttribute")]
[JsonRpcMethod]
internal void InternalMethodWithAttribute()
{
this.ServerMethodReached.Set();
Expand Down Expand Up @@ -2016,6 +2137,18 @@ public class XAndYFields
#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter
}

[DataContract]
public class VAndWFields
{
// We disable SA1307 because we must use lowercase members as required to match the parameter names.
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
[DataMember]
public int v;
[DataMember]
public int w;
#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter
}

[DataContract]
public class XAndYFieldsWithProgress
{
Expand Down
31 changes: 24 additions & 7 deletions src/StreamJsonRpc/JsonMessageFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -596,17 +596,34 @@ internal JsonRpcRequest(JsonMessageFormatter formatter)

public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan<ParameterInfo> parameters, Span<object> typedArguments)
{
// Special support for accepting a single JToken instead of all parameters individually.
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(JToken) && this.NamedArguments != null)
if (parameters.Length == 1 && this.NamedArguments != null)
{
var obj = new JObject();
foreach (KeyValuePair<string, object> property in this.NamedArguments)
// Special support for accepting a single JToken instead of all parameters individually.
if (parameters[0].ParameterType == typeof(JToken))
{
obj.Add(new JProperty(property.Key, property.Value));
var obj = new JObject();
foreach (KeyValuePair<string, object> property in this.NamedArguments)
{
obj.Add(new JProperty(property.Key, property.Value));
}

typedArguments[0] = obj;
return ArgumentMatchResult.Success;
}

typedArguments[0] = obj;
return ArgumentMatchResult.Success;
// Support for opt-in to deserializing all named arguments into a single parameter.
JsonRpcMethodAttribute attribute = this.formatter.rpc.GetJsonRpcMethodAttribute(this.Method, parameters);
if (attribute?.UseSingleObjectParameterDeserialization ?? false)
{
var obj = new JObject();
foreach (KeyValuePair<string, object> property in this.NamedArguments)
{
obj.Add(new JProperty(property.Key, property.Value));
}

typedArguments[0] = obj.ToObject(parameters[0].ParameterType, this.formatter.JsonSerializer);
return ArgumentMatchResult.Success;
}
}

return base.TryGetTypedArguments(parameters, typedArguments);
Expand Down
50 changes: 45 additions & 5 deletions src/StreamJsonRpc/JsonRpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -737,16 +737,31 @@ public void AddLocalRpcMethod(string rpcMethodName, Delegate handler)
/// This method may accept parameters from the incoming JSON-RPC message.
/// </param>
/// <param name="target">An instance of the type that defines <paramref name="handler"/> which should handle the invocation.</param>
public void AddLocalRpcMethod(string rpcMethodName, MethodInfo handler, object target)
public void AddLocalRpcMethod(string rpcMethodName, MethodInfo handler, object target) => this.AddLocalRpcMethod(handler, target, new JsonRpcMethodAttribute(rpcMethodName));

/// <summary>
/// Adds a handler for an RPC method with a given name.
/// </summary>
/// <param name="handler">
/// The method or delegate to invoke when a matching RPC message arrives.
/// This method may accept parameters from the incoming JSON-RPC message.
/// </param>
/// <param name="target">An instance of the type that defines <paramref name="handler"/> which should handle the invocation.</param>
/// <param name="methodRpcSettings">
/// A description for how this method should be treated.
/// It need not be an attribute that was actually applied to <paramref name="handler"/>.
/// An attribute will *not* be discovered via reflection on the <paramref name="handler"/>, even if this value is <c>null</c>.
/// </param>
public void AddLocalRpcMethod(MethodInfo handler, object target, JsonRpcMethodAttribute methodRpcSettings)
{
Requires.NotNullOrEmpty(rpcMethodName, nameof(rpcMethodName));
Requires.NotNull(handler, nameof(handler));
Requires.Argument(handler.IsStatic == (target == null), nameof(target), Resources.TargetObjectAndMethodStaticFlagMismatch);

this.ThrowIfConfigurationLocked();
string rpcMethodName = methodRpcSettings?.Name ?? handler.Name;
lock (this.syncObject)
{
var methodTarget = new MethodSignatureAndTarget(handler, target);
var methodTarget = new MethodSignatureAndTarget(handler, target, methodRpcSettings);
this.TraceLocalMethodAdded(rpcMethodName, methodTarget);
if (this.targetRequestMethodToClrMethodMap.TryGetValue(rpcMethodName, out List<MethodSignatureAndTarget> existingList))
{
Expand All @@ -764,6 +779,30 @@ public void AddLocalRpcMethod(string rpcMethodName, MethodInfo handler, object t
}
}

/// <summary>
/// Gets the <see cref="JsonRpcMethodAttribute"/> for a previously discovered RPC method, if there is one.
/// </summary>
/// <param name="methodName">The name of the method for which the attribute is sought.</param>
/// <param name="parameters">
/// The list of parameters found on the method, as they may be given to <see cref="JsonRpcRequest.TryGetTypedArguments(ReadOnlySpan{ParameterInfo}, Span{object})"/>.
/// Note this list may omit some special parameters such as a trailing <see cref="CancellationToken"/>.
/// </param>
public JsonRpcMethodAttribute GetJsonRpcMethodAttribute(string methodName, ReadOnlySpan<ParameterInfo> parameters)
{
if (this.targetRequestMethodToClrMethodMap.TryGetValue(methodName, out List<MethodSignatureAndTarget> existingList))
{
foreach (MethodSignatureAndTarget entry in existingList)
{
if (entry.Signature.MatchesParametersExcludingCancellationToken(parameters))
{
return entry.Signature.Attribute;
}
}
}

return null;
}

/// <summary>
/// Starts listening to incoming messages.
/// </summary>
Expand Down Expand Up @@ -1275,8 +1314,10 @@ private static Dictionary<string, List<MethodSignatureAndTarget>> GetRequestMeth
requestMethodToClrMethodNameMap.Add(requestName, method.Name);
}

JsonRpcMethodAttribute attribute = mapping.FindAttribute(method);

// Skip this method if its signature matches one from a derived type we have already scanned.
MethodSignatureAndTarget methodTarget = new MethodSignatureAndTarget(method, target);
MethodSignatureAndTarget methodTarget = new MethodSignatureAndTarget(method, target, attribute);
if (methodTargetList.Contains(methodTarget))
{
continue;
Expand All @@ -1286,7 +1327,6 @@ private static Dictionary<string, List<MethodSignatureAndTarget>> GetRequestMeth

// If no explicit attribute has been applied, and the method ends with Async,
// register a request method name that does not include Async as well.
JsonRpcMethodAttribute attribute = mapping.FindAttribute(method);
if (attribute == null && method.Name.EndsWith(ImpliedMethodNameAsyncSuffix, StringComparison.Ordinal))
{
string nonAsyncMethodName = method.Name.Substring(0, method.Name.Length - ImpliedMethodNameAsyncSuffix.Length);
Expand Down
5 changes: 5 additions & 0 deletions src/StreamJsonRpc/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
StreamJsonRpc.JsonRpc.AddLocalRpcMethod(System.Reflection.MethodInfo handler, object target, StreamJsonRpc.JsonRpcMethodAttribute methodRpcSettings) -> void
StreamJsonRpc.JsonRpc.GetJsonRpcMethodAttribute(string methodName, System.ReadOnlySpan<System.Reflection.ParameterInfo> parameters) -> StreamJsonRpc.JsonRpcMethodAttribute
StreamJsonRpc.JsonRpc.InvokeCoreAsync<TResult>(StreamJsonRpc.RequestId id, string targetName, System.Collections.Generic.IReadOnlyList<object> arguments, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<TResult>
StreamJsonRpc.JsonRpc.InvokeCoreAsync<TResult>(StreamJsonRpc.RequestId id, string targetName, System.Collections.Generic.IReadOnlyList<object> arguments, System.Threading.CancellationToken cancellationToken, bool isParameterObject) -> System.Threading.Tasks.Task<TResult>
StreamJsonRpc.JsonRpcMethodAttribute.JsonRpcMethodAttribute() -> void
StreamJsonRpc.JsonRpcMethodAttribute.UseSingleObjectParameterDeserialization.get -> bool
StreamJsonRpc.JsonRpcMethodAttribute.UseSingleObjectParameterDeserialization.set -> void
StreamJsonRpc.Protocol.JsonRpcError.RequestId.get -> StreamJsonRpc.RequestId
StreamJsonRpc.Protocol.JsonRpcError.RequestId.set -> void
StreamJsonRpc.Protocol.JsonRpcRequest.RequestId.get -> StreamJsonRpc.RequestId
Expand Down
15 changes: 14 additions & 1 deletion src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,21 @@ public JsonRpcMethodAttribute(string name)
}

/// <summary>
/// Gets the replacement name of a method.
/// Initializes a new instance of the <see cref="JsonRpcMethodAttribute"/> class.
/// </summary>
public JsonRpcMethodAttribute()
{
}

/// <summary>
/// Gets the public RPC name by which this method will be invoked.
/// </summary>
/// <value>May be <c>null</c> if the method's name has not been overridden.</value>
public string Name { get; }

/// <summary>
/// Gets or sets a value indicating whether JSON-RPC named arguments should all be deserialized into this method's first parameter.
/// </summary>
public bool UseSingleObjectParameterDeserialization { get; set; }
}
}
23 changes: 22 additions & 1 deletion src/StreamJsonRpc/Reflection/MethodSignature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@ internal sealed class MethodSignature : IEquatable<MethodSignature>
/// </summary>
private ParameterInfo[] parameters;

internal MethodSignature(MethodInfo methodInfo)
internal MethodSignature(MethodInfo methodInfo, JsonRpcMethodAttribute attribute)
{
Requires.NotNull(methodInfo, nameof(methodInfo));
this.MethodInfo = methodInfo;
this.Attribute = attribute;
}

internal MethodInfo MethodInfo { get; }

internal JsonRpcMethodAttribute Attribute { get; }

internal ParameterInfo[] Parameters => this.parameters ?? (this.parameters = this.MethodInfo.GetParameters() ?? EmptyParameterInfoArray);

internal bool IsPublic => this.MethodInfo.IsPublic;
Expand Down Expand Up @@ -113,6 +116,24 @@ public override string ToString()
return this.DebuggerDisplay;
}

internal bool MatchesParametersExcludingCancellationToken(ReadOnlySpan<ParameterInfo> parameters)
{
if (this.TotalParamCountExcludingCancellationToken == parameters.Length)
{
for (int i = 0; i < parameters.Length; i++)
{
if (parameters[i].ParameterType != this.Parameters[i].ParameterType)
{
return false;
}
}

return true;
}

return false;
}

private static bool IsCancellationToken(ParameterInfo parameter) => parameter?.ParameterType.Equals(typeof(CancellationToken)) ?? false;
}
}
Loading