From 044bc23d071030e6f9f9d9b131997223457ada0e Mon Sep 17 00:00:00 2001 From: "Miguel Lopez (MILOPEZC)" Date: Thu, 17 Oct 2019 13:21:23 -0700 Subject: [PATCH 1/8] Support for deserializing params *object* as single first parameter --- src/StreamJsonRpc.Tests/JsonRpcTests.cs | 194 +++++++++++++++++++++- src/StreamJsonRpc/JsonMessageFormatter.cs | 52 +++++- 2 files changed, 237 insertions(+), 9 deletions(-) diff --git a/src/StreamJsonRpc.Tests/JsonRpcTests.cs b/src/StreamJsonRpc.Tests/JsonRpcTests.cs index 29e8b3da..ef0e57ea 100644 --- a/src/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/src/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -942,11 +942,119 @@ public async Task InvokeWithParameterObject_ProgressAndDefaultParameters() } [SkippableFact] - public async Task InvokeWithParameterObject_ClassIncludingProgressProperty() + public async Task InvokeWithSingleObjectParameter_SendingWithProgressFieldButServerDoesNotExpectIt() + { + Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); + + int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithFieldsParameter), new XAndYFieldsWithProgress { x = 2, y = 5, p = new Progress() }, this.TimeoutToken); + Assert.Equal(7, sum); + } + + [SkippableFact] + public async Task InvokeWithSingleObjectParameter_SendingWithProgressFieldButServerDoesNotExpectItAndDefaultParam() + { + Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); + + int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithFieldsParameter), new XAndYFieldsWithProgress { x = 2, p = new Progress() }, this.TimeoutToken); + Assert.Equal(2, sum); + } + + [SkippableFact] + public async Task InvokeWithSingleObjectParameter_SendingWithProgressPropertyButServerDoesNotExpectIt() + { + Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); + + int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithPropertiesParameter), new XAndYPropertiesWithProgress { X = 2, Y = 5, P = new Progress() }, this.TimeoutToken); + Assert.Equal(7, sum); + } + + [SkippableFact] + public async Task InvokeWithSingleObjectParameter_SendingWithProgressPropertyButServerDoesNotExpectItAndDefaultParam() + { + Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); + + int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithPropertiesParameter), new XAndYPropertiesWithProgress { X = 2, P = new Progress() }, this.TimeoutToken); + Assert.Equal(2, sum); + } + + [SkippableFact] + public async Task InvokeWithSingleObjectParameter_SendingOnlyOneMatchingProperty() + { + Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); + + int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithPropertiesParameter), new { X = 2 }, this.TimeoutToken); + Assert.Equal(2, sum); + } + + [SkippableFact] + public async Task InvokeWithSingleNonObjectParameter() + { + int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithOneNonObjectParameter), new { x = 2 }, this.TimeoutToken); + Assert.Equal(2, sum); + } + + [SkippableFact] + public async Task InvokeWithSingleObjectParameter_SendingOnlyOneMatchingPropertyAndSomeThatDontMatch() + { + Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); + + int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithPropertiesParameter), new { X = 2, W = 4, Z = 7 }, this.TimeoutToken); + Assert.Equal(2, sum); + } + + [SkippableFact] + public async Task InvokeWithSingleObjectParameter_SendingNoMatchingProperty() + { + Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); + + await Assert.ThrowsAsync(async () => await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithPropertiesParameter), new { Z = 2 }, this.TimeoutToken)); + } + + [SkippableFact] + public async Task InvokeWithSingleObjectParameter_SendingWithProgressProperty() { Skip.If(this.clientMessageFormatter is MessagePackFormatter, "IProgress serialization is not supported for MessagePack"); - int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithProgressAndMoreParameters), new XAndYFieldsWithProgress { x = 2, y = 5, p = new Progress() }, this.TimeoutToken); + int report = 0; + var progress = new ProgressWithCompletion(n => report += n); + + int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithProgressParameter), new XAndYPropertiesWithProgress { X = 2, Y = 5, P = progress }, this.TimeoutToken); + + await progress.WaitAsync(); + + Assert.Equal(7, report); + Assert.Equal(7, sum); + } + + [SkippableFact] + public async Task InvokeWithSingleObjectParameter_SendingWithProgressPropertyAndDefaultParam() + { + Skip.If(this.clientMessageFormatter is MessagePackFormatter, "IProgress serialization is not supported for MessagePack"); + + int report = 0; + var progress = new ProgressWithCompletion(n => report += n); + + int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithProgressParameter), new XAndYPropertiesWithProgress { X = 2, P = progress }, this.TimeoutToken); + + await progress.WaitAsync(); + + Assert.Equal(2, report); + Assert.Equal(2, sum); + } + + [SkippableFact] + public async Task InvokeWithSingleObjectParameter_SendingWithProgressPropertyAndNoMatchingProperties() + { + Skip.If(this.clientMessageFormatter is MessagePackFormatter, "IProgress serialization is not supported for MessagePack"); + + int report = 0; + var progress = new ProgressWithCompletion(n => report += n); + + int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithProgressParameter), new { X = 2, Y = 5, Z = 9, P = progress }, this.TimeoutToken); + + await progress.WaitAsync(); + + Assert.Equal(7, report); Assert.Equal(7, sum); } @@ -1657,6 +1765,27 @@ public static int MethodWithDefaultParameter(int x, int y = 10) return x + y; } + public static int MethodWithOneNonObjectParameter(int x) + { + return x; + } + + public static int MethodWithObjectWithFieldsParameter(XAndYFields fields) + { + return fields.x + fields.y; + } + + public static int MethodWithObjectWithPropertiesParameter(XAndYProperties props) + { + return props.X + props.Y; + } + + public static int MethodWithObjectWithProgressParameter(XAndYPropertiesWithProgress props) + { + props.P.Report(props.X + props.Y); + return props.X + props.Y; + } + public static int MethodWithProgressParameter(IProgress p) { p.Report(1); @@ -1689,6 +1818,17 @@ public static int MethodWithInvalidProgressParameter(Progress p) return 1; } + public static int MethodWithOnlyOneFuzzyParameter(int fuzzyX) + { + return fuzzyX * 2; + } + + public static int MethodWithTwoFuzzyParameters(int fuzzyX, int fuzzyY) + { + int sum = fuzzyX + fuzzyY; + return sum; + } + 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); @@ -2016,6 +2156,27 @@ public class XAndYFields #pragma warning restore SA1307 // Accessible fields should begin with upper-case letter } + [DataContract] + public class XAndYProperties + { + // 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 X + { + get; + set; + } + + [DataMember] + public int Y + { + get; + set; + } +#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter + } + [DataContract] public class XAndYFieldsWithProgress { @@ -2030,6 +2191,35 @@ public class XAndYFieldsWithProgress #pragma warning restore SA1307 // Accessible fields should begin with upper-case letter } + [DataContract] + public class XAndYPropertiesWithProgress + { + // 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 X + { + get; + set; + } + + [DataMember] + public int Y + { + get; + set; + } + + + [DataMember] + public IProgress P + { + get; + set; + } +#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter + } + internal class InternalClass { } diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index 2676a1b4..134d7609 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -583,17 +583,55 @@ internal JsonRpcRequest(JsonMessageFormatter formatter) public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan parameters, Span 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 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 property in this.NamedArguments) + { + obj.Add(new JProperty(property.Key, property.Value)); + } + + typedArguments[0] = obj; + return ArgumentMatchResult.Success; } - typedArguments[0] = obj; - return ArgumentMatchResult.Success; + // Check if it is constructable with default constructor + if (parameters[0].ParameterType.GetConstructor(Type.EmptyTypes) != null && !parameters[0].ParameterType.IsAbstract) + { + // If the method is expecting only one parameter we should try to create the expected object with the given arguments + var obj = Activator.CreateInstance(parameters[0].ParameterType); + bool match = false; + + // Loop value-key pairs from arguments + foreach (KeyValuePair property in this.NamedArguments) + { + // If argument name matches property name, assign the value to that property + PropertyInfo propertyInfo = parameters[0].ParameterType.GetProperty(property.Key); + JToken value = (JToken)property.Value; + if (propertyInfo != null) + { + propertyInfo.SetValue(obj, value.ToObject(propertyInfo.PropertyType, this.formatter.JsonSerializer)); + match = true; + } + else + { + FieldInfo fieldInfo = parameters[0].ParameterType.GetField(property.Key); + if (fieldInfo != null) + { + fieldInfo.SetValue(obj, value.ToObject(fieldInfo.FieldType, this.formatter.JsonSerializer)); + match = true; + } + } + } + + typedArguments[0] = obj; + + // If one of the arguments matched the parameters we return ArgumentMatchResult.Success + return match ? ArgumentMatchResult.Success : base.TryGetTypedArguments(parameters, typedArguments); + } } return base.TryGetTypedArguments(parameters, typedArguments); From 21f54ad4b67c8b2b5da8c55452965a35ec069c6d Mon Sep 17 00:00:00 2001 From: "Miguel Lopez (MILOPEZC)" Date: Thu, 17 Oct 2019 13:28:55 -0700 Subject: [PATCH 2/8] Removed unnecessary test methods --- src/StreamJsonRpc.Tests/JsonRpcTests.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/StreamJsonRpc.Tests/JsonRpcTests.cs b/src/StreamJsonRpc.Tests/JsonRpcTests.cs index ef0e57ea..959bb7b2 100644 --- a/src/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/src/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -986,7 +986,7 @@ public async Task InvokeWithSingleObjectParameter_SendingOnlyOneMatchingProperty Assert.Equal(2, sum); } - [SkippableFact] + [Fact] public async Task InvokeWithSingleNonObjectParameter() { int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithOneNonObjectParameter), new { x = 2 }, this.TimeoutToken); @@ -1818,17 +1818,6 @@ public static int MethodWithInvalidProgressParameter(Progress p) return 1; } - public static int MethodWithOnlyOneFuzzyParameter(int fuzzyX) - { - return fuzzyX * 2; - } - - public static int MethodWithTwoFuzzyParameters(int fuzzyX, int fuzzyY) - { - int sum = fuzzyX + fuzzyY; - return sum; - } - 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); From 6eb534a300bf4fbf6959c5fc425b04d0d8edebda Mon Sep 17 00:00:00 2001 From: "Miguel Lopez (MILOPEZC)" Date: Thu, 17 Oct 2019 13:56:52 -0700 Subject: [PATCH 3/8] Removed extra blank lines --- src/StreamJsonRpc.Tests/JsonRpcTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/StreamJsonRpc.Tests/JsonRpcTests.cs b/src/StreamJsonRpc.Tests/JsonRpcTests.cs index 959bb7b2..33d235a7 100644 --- a/src/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/src/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -2199,7 +2199,6 @@ public int Y set; } - [DataMember] public IProgress P { From f9ae985d0585af9be28e2c76b2233e1861e93536 Mon Sep 17 00:00:00 2001 From: "Miguel Lopez (MILOPEZC)" Date: Fri, 1 Nov 2019 13:45:16 -0700 Subject: [PATCH 4/8] Refactored single object deserialization logic and updated tests --- src/StreamJsonRpc.Tests/JsonRpcTests.cs | 164 +++++------------- src/StreamJsonRpc/JsonMessageFormatter.cs | 34 +--- src/StreamJsonRpc/JsonRpc.cs | 26 +++ src/StreamJsonRpc/PublicAPI.Shipped.txt | 2 +- src/StreamJsonRpc/PublicAPI.Unshipped.txt | 1 + .../Reflection/JsonRpcMethodAttribute.cs | 9 +- 6 files changed, 85 insertions(+), 151 deletions(-) diff --git a/src/StreamJsonRpc.Tests/JsonRpcTests.cs b/src/StreamJsonRpc.Tests/JsonRpcTests.cs index 33d235a7..1b1d5a4f 100644 --- a/src/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/src/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -942,115 +942,54 @@ public async Task InvokeWithParameterObject_ProgressAndDefaultParameters() } [SkippableFact] - public async Task InvokeWithSingleObjectParameter_SendingWithProgressFieldButServerDoesNotExpectIt() + 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(nameof(Server.MethodWithObjectWithFieldsParameter), new XAndYFieldsWithProgress { x = 2, y = 5, p = new Progress() }, this.TimeoutToken); + int sum = await this.clientRpc.InvokeWithParameterObjectAsync("test/MethodWithSingleObjectParameter", new XAndYFields { x = 2, y = 5 }, this.TimeoutToken); Assert.Equal(7, sum); } - [SkippableFact] - public async Task InvokeWithSingleObjectParameter_SendingWithProgressFieldButServerDoesNotExpectItAndDefaultParam() + [Fact] + public async Task InvokeWithSingleObjectParameter_ServerMethodExpectsObjectButDoesNotSetDeserializationProperty() { - Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); - - int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithFieldsParameter), new XAndYFieldsWithProgress { x = 2, p = new Progress() }, this.TimeoutToken); - Assert.Equal(2, sum); + await Assert.ThrowsAsync(async () => await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithSingleObjectParameterWithoutDeserializationProperty), new XAndYFields { x = 2, y = 5 }, this.TimeoutToken)); } [SkippableFact] - public async Task InvokeWithSingleObjectParameter_SendingWithProgressPropertyButServerDoesNotExpectIt() + 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(nameof(Server.MethodWithObjectWithPropertiesParameter), new XAndYPropertiesWithProgress { X = 2, Y = 5, P = new Progress() }, this.TimeoutToken); - Assert.Equal(7, sum); - } + int sum = await this.clientRpc.InvokeWithParameterObjectAsync("test/MethodWithSingleObjectParameterVAndW", new XAndYFields { x = 2, y = 5 }, this.TimeoutToken); - [SkippableFact] - public async Task InvokeWithSingleObjectParameter_SendingWithProgressPropertyButServerDoesNotExpectItAndDefaultParam() - { - Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); - - int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithPropertiesParameter), new XAndYPropertiesWithProgress { X = 2, P = new Progress() }, this.TimeoutToken); - Assert.Equal(2, sum); - } - - [SkippableFact] - public async Task InvokeWithSingleObjectParameter_SendingOnlyOneMatchingProperty() - { - Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); - - int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithPropertiesParameter), new { X = 2 }, this.TimeoutToken); - Assert.Equal(2, sum); + Assert.Equal(0, sum); } [Fact] - public async Task InvokeWithSingleNonObjectParameter() + public async Task InvokeWithSingleObjectParameter_ServerMethodSetDeserializationPropertyButExpectMoreThanOneParameter() { - int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithOneNonObjectParameter), new { x = 2 }, this.TimeoutToken); - Assert.Equal(2, sum); - } - - [SkippableFact] - public async Task InvokeWithSingleObjectParameter_SendingOnlyOneMatchingPropertyAndSomeThatDontMatch() - { - Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); - - int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithPropertiesParameter), new { X = 2, W = 4, Z = 7 }, this.TimeoutToken); - Assert.Equal(2, sum); + await Assert.ThrowsAsync(async () => await this.clientRpc.InvokeWithParameterObjectAsync("test/MethodWithObjectAndExtraParameters", new XAndYFields { x = 2, y = 5 }, this.TimeoutToken)); } [SkippableFact] - public async Task InvokeWithSingleObjectParameter_SendingNoMatchingProperty() + public async Task InvokeWithSingleObjectParameter_SendingExpectedObjectAndCancellationToken() { Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); - await Assert.ThrowsAsync(async () => await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithPropertiesParameter), new { Z = 2 }, this.TimeoutToken)); - } - - [SkippableFact] - public async Task InvokeWithSingleObjectParameter_SendingWithProgressProperty() - { - Skip.If(this.clientMessageFormatter is MessagePackFormatter, "IProgress serialization is not supported for MessagePack"); - - int report = 0; - var progress = new ProgressWithCompletion(n => report += n); - - int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithProgressParameter), new XAndYPropertiesWithProgress { X = 2, Y = 5, P = progress }, this.TimeoutToken); - - await progress.WaitAsync(); - - Assert.Equal(7, report); + int sum = await this.clientRpc.InvokeWithParameterObjectAsync("test/MethodWithSingleObjectParameterAndCancellationToken", new XAndYFields { x = 2, y = 5 }, this.TimeoutToken); Assert.Equal(7, sum); } [SkippableFact] - public async Task InvokeWithSingleObjectParameter_SendingWithProgressPropertyAndDefaultParam() - { - Skip.If(this.clientMessageFormatter is MessagePackFormatter, "IProgress serialization is not supported for MessagePack"); - - int report = 0; - var progress = new ProgressWithCompletion(n => report += n); - - int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithProgressParameter), new XAndYPropertiesWithProgress { X = 2, P = progress }, this.TimeoutToken); - - await progress.WaitAsync(); - - Assert.Equal(2, report); - Assert.Equal(2, sum); - } - - [SkippableFact] - public async Task InvokeWithSingleObjectParameter_SendingWithProgressPropertyAndNoMatchingProperties() + public async Task InvokeWithSingleObjectParameter_SendingWithProgressProperty() { Skip.If(this.clientMessageFormatter is MessagePackFormatter, "IProgress serialization is not supported for MessagePack"); int report = 0; var progress = new ProgressWithCompletion(n => report += n); - int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithObjectWithProgressParameter), new { X = 2, Y = 5, Z = 9, P = progress }, this.TimeoutToken); + int sum = await this.clientRpc.InvokeWithParameterObjectAsync("test/MethodWithSingleObjectParameterWithProgress", new XAndYFieldsWithProgress { x = 2, y = 5, p = progress }, this.TimeoutToken); await progress.WaitAsync(); @@ -1770,20 +1709,40 @@ public static int MethodWithOneNonObjectParameter(int x) return x; } - public static int MethodWithObjectWithFieldsParameter(XAndYFields fields) + [JsonRpcMethod("test/MethodWithSingleObjectParameter", true)] + public static int MethodWithSingleObjectParameter(XAndYFields fields) { return fields.x + fields.y; } - public static int MethodWithObjectWithPropertiesParameter(XAndYProperties props) + public static int MethodWithSingleObjectParameterWithoutDeserializationProperty(XAndYFields fields) { - return props.X + props.Y; + return fields.x + fields.y; } - public static int MethodWithObjectWithProgressParameter(XAndYPropertiesWithProgress props) + [JsonRpcMethod("test/MethodWithSingleObjectParameterVAndW", true)] + public static int MethodWithSingleObjectParameterVAndW(VAndWFields fields) { - props.P.Report(props.X + props.Y); - return props.X + props.Y; + return fields.v + fields.w; + } + + [JsonRpcMethod("test/MethodWithSingleObjectParameterAndCancellationToken", true)] + public static int MethodWithSingleObjectParameterAndCancellationToken(XAndYFields fields, CancellationToken token) + { + return fields.x + fields.y; + } + + [JsonRpcMethod("test/MethodWithSingleObjectParameterWithProgress", true)] + public static int MethodWithSingleObjectParameterWithProgress(XAndYFieldsWithProgress fields) + { + fields.p.Report(fields.x + fields.y); + return fields.x + fields.y; + } + + [JsonRpcMethod("test/MethodWithObjectAndExtraParameters", true)] + public static int MethodWithObjectAndExtraParameters(XAndYFields fields, int anotherParameter) + { + return fields.x + fields.y + anotherParameter; } public static int MethodWithProgressParameter(IProgress p) @@ -2146,23 +2105,14 @@ public class XAndYFields } [DataContract] - public class XAndYProperties + 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 X - { - get; - set; - } - + public int v; [DataMember] - public int Y - { - get; - set; - } + public int w; #pragma warning restore SA1307 // Accessible fields should begin with upper-case letter } @@ -2180,34 +2130,6 @@ public class XAndYFieldsWithProgress #pragma warning restore SA1307 // Accessible fields should begin with upper-case letter } - [DataContract] - public class XAndYPropertiesWithProgress - { - // 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 X - { - get; - set; - } - - [DataMember] - public int Y - { - get; - set; - } - - [DataMember] - public IProgress P - { - get; - set; - } -#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter - } - internal class InternalClass { } diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index 134d7609..6a0ca4f9 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -598,39 +598,17 @@ public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan property in this.NamedArguments) { - // If argument name matches property name, assign the value to that property - PropertyInfo propertyInfo = parameters[0].ParameterType.GetProperty(property.Key); - JToken value = (JToken)property.Value; - if (propertyInfo != null) - { - propertyInfo.SetValue(obj, value.ToObject(propertyInfo.PropertyType, this.formatter.JsonSerializer)); - match = true; - } - else - { - FieldInfo fieldInfo = parameters[0].ParameterType.GetField(property.Key); - if (fieldInfo != null) - { - fieldInfo.SetValue(obj, value.ToObject(fieldInfo.FieldType, this.formatter.JsonSerializer)); - match = true; - } - } + obj.Add(new JProperty(property.Key, property.Value)); } - typedArguments[0] = obj; - - // If one of the arguments matched the parameters we return ArgumentMatchResult.Success - return match ? ArgumentMatchResult.Success : base.TryGetTypedArguments(parameters, typedArguments); + typedArguments[0] = obj.ToObject(parameters[0].ParameterType, this.formatter.JsonSerializer); + return ArgumentMatchResult.Success; } } diff --git a/src/StreamJsonRpc/JsonRpc.cs b/src/StreamJsonRpc/JsonRpc.cs index ba603ed0..b1118d96 100644 --- a/src/StreamJsonRpc/JsonRpc.cs +++ b/src/StreamJsonRpc/JsonRpc.cs @@ -1048,6 +1048,32 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Tries to obtain the info of a method. + /// + internal bool MethodSupportsSingleParameterObjectDeserialization(string methodName, Type parameterType) + { + // Get method definitions + if (this.targetRequestMethodToClrMethodMap.TryGetValue(methodName, out List existingList)) + { + // Check if there is one definition with one parameter of the expected type. + if (existingList.Any(m => m.Signature.TotalParamCountExcludingCancellationToken == 1 && m.Signature.Parameters[0].ParameterType == parameterType)) + { + // Try to obtain the JsonRpcMethod attribute and check if the method supports single parameter object deserialization + MethodSignatureAndTarget methodSignatureAndTarget = existingList.First(m => m.Signature.TotalParamCountExcludingCancellationToken == 1 && m.Signature.Parameters[0].ParameterType == parameterType); + var mapping = new JsonRpc.MethodNameMap(methodSignatureAndTarget.Target.GetType().GetTypeInfo()); + JsonRpcMethodAttribute rpcMethod = mapping.FindAttribute(methodSignatureAndTarget.Signature.MethodInfo); + + if (rpcMethod != null && rpcMethod.UseSingleObjectParameterDeserialization) + { + return true; + } + } + } + + return false; + } + /// /// Disposes managed and native resources held by this instance. /// diff --git a/src/StreamJsonRpc/PublicAPI.Shipped.txt b/src/StreamJsonRpc/PublicAPI.Shipped.txt index 5f6dd658..7565c57e 100644 --- a/src/StreamJsonRpc/PublicAPI.Shipped.txt +++ b/src/StreamJsonRpc/PublicAPI.Shipped.txt @@ -41,7 +41,7 @@ StreamJsonRpc.JsonRpc.InvokeAsync(string targetName, object argument) - StreamJsonRpc.JsonRpc.InvokeWithParameterObjectAsync(string targetName, object argument = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task StreamJsonRpc.JsonRpc.NotifyWithParameterObjectAsync(string targetName, object argument = null) -> System.Threading.Tasks.Task StreamJsonRpc.JsonRpcMethodAttribute -StreamJsonRpc.JsonRpcMethodAttribute.JsonRpcMethodAttribute(string name) -> void +StreamJsonRpc.JsonRpcMethodAttribute.JsonRpcMethodAttribute(string name, bool useSingleObjectParameterDeserialization = false) -> void StreamJsonRpc.JsonRpcMethodAttribute.Name.get -> string StreamJsonRpc.JsonRpc.AddLocalRpcMethod(string rpcMethodName, System.Delegate handler) -> void StreamJsonRpc.JsonRpc.AddLocalRpcMethod(string rpcMethodName, System.Reflection.MethodInfo handler, object target) -> void diff --git a/src/StreamJsonRpc/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/PublicAPI.Unshipped.txt index 5caadfc9..00389f57 100644 --- a/src/StreamJsonRpc/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/PublicAPI.Unshipped.txt @@ -15,6 +15,7 @@ StreamJsonRpc.JsonRpc.AddRemoteRpcTarget(StreamJsonRpc.JsonRpc remoteTarget) -> StreamJsonRpc.JsonRpc.Attach(System.Type interfaceType) -> object StreamJsonRpc.JsonRpc.Attach(System.Type interfaceType, StreamJsonRpc.JsonRpcProxyOptions options) -> object StreamJsonRpc.JsonRpc.TraceEvents.ProgressNotificationError = 16 -> StreamJsonRpc.JsonRpc.TraceEvents +StreamJsonRpc.JsonRpcMethodAttribute.UseSingleObjectParameterDeserialization.get -> bool StreamJsonRpc.Reflection.MessageFormatterDuplexPipeTracker StreamJsonRpc.Reflection.MessageFormatterDuplexPipeTracker.Dispose() -> void StreamJsonRpc.Reflection.MessageFormatterDuplexPipeTracker.GetPipe(int? token) -> System.IO.Pipelines.IDuplexPipe diff --git a/src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs b/src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs index 417d8e6b..716003d6 100644 --- a/src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs +++ b/src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs @@ -27,16 +27,23 @@ public class JsonRpcMethodAttribute : Attribute /// Initializes a new instance of the class. /// /// Replacement name of a method. - public JsonRpcMethodAttribute(string name) + /// Indicates whether the method supports deserialization for single object parameter. + public JsonRpcMethodAttribute(string name, bool useSingleObjectParameterDeserialization = false) { Requires.NotNullOrWhiteSpace(name, nameof(name)); this.Name = name; + this.UseSingleObjectParameterDeserialization = useSingleObjectParameterDeserialization; } /// /// Gets the replacement name of a method. /// public string Name { get; } + + /// + /// Gets a value indicating whether the method supports deserialization for sinlge object parameter. + /// + public bool UseSingleObjectParameterDeserialization { get; } } } From c3884d5a13add30c906d742d5c19a8c68d50a885 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 2 Nov 2019 07:01:17 -0600 Subject: [PATCH 5/8] Revert public API breaking change to attribute --- src/StreamJsonRpc.Tests/JsonRpcTests.cs | 10 +++++----- src/StreamJsonRpc/PublicAPI.Shipped.txt | 2 +- src/StreamJsonRpc/PublicAPI.Unshipped.txt | 1 + src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs | 8 +++----- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/StreamJsonRpc.Tests/JsonRpcTests.cs b/src/StreamJsonRpc.Tests/JsonRpcTests.cs index 1b1d5a4f..cffcf0cb 100644 --- a/src/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/src/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -1709,7 +1709,7 @@ public static int MethodWithOneNonObjectParameter(int x) return x; } - [JsonRpcMethod("test/MethodWithSingleObjectParameter", true)] + [JsonRpcMethod("test/MethodWithSingleObjectParameter", UseSingleObjectParameterDeserialization = true)] public static int MethodWithSingleObjectParameter(XAndYFields fields) { return fields.x + fields.y; @@ -1720,26 +1720,26 @@ public static int MethodWithSingleObjectParameterWithoutDeserializationProperty( return fields.x + fields.y; } - [JsonRpcMethod("test/MethodWithSingleObjectParameterVAndW", true)] + [JsonRpcMethod("test/MethodWithSingleObjectParameterVAndW", UseSingleObjectParameterDeserialization = true)] public static int MethodWithSingleObjectParameterVAndW(VAndWFields fields) { return fields.v + fields.w; } - [JsonRpcMethod("test/MethodWithSingleObjectParameterAndCancellationToken", true)] + [JsonRpcMethod("test/MethodWithSingleObjectParameterAndCancellationToken", UseSingleObjectParameterDeserialization = true)] public static int MethodWithSingleObjectParameterAndCancellationToken(XAndYFields fields, CancellationToken token) { return fields.x + fields.y; } - [JsonRpcMethod("test/MethodWithSingleObjectParameterWithProgress", true)] + [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", true)] + [JsonRpcMethod("test/MethodWithObjectAndExtraParameters", UseSingleObjectParameterDeserialization = true)] public static int MethodWithObjectAndExtraParameters(XAndYFields fields, int anotherParameter) { return fields.x + fields.y + anotherParameter; diff --git a/src/StreamJsonRpc/PublicAPI.Shipped.txt b/src/StreamJsonRpc/PublicAPI.Shipped.txt index 7565c57e..5f6dd658 100644 --- a/src/StreamJsonRpc/PublicAPI.Shipped.txt +++ b/src/StreamJsonRpc/PublicAPI.Shipped.txt @@ -41,7 +41,7 @@ StreamJsonRpc.JsonRpc.InvokeAsync(string targetName, object argument) - StreamJsonRpc.JsonRpc.InvokeWithParameterObjectAsync(string targetName, object argument = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task StreamJsonRpc.JsonRpc.NotifyWithParameterObjectAsync(string targetName, object argument = null) -> System.Threading.Tasks.Task StreamJsonRpc.JsonRpcMethodAttribute -StreamJsonRpc.JsonRpcMethodAttribute.JsonRpcMethodAttribute(string name, bool useSingleObjectParameterDeserialization = false) -> void +StreamJsonRpc.JsonRpcMethodAttribute.JsonRpcMethodAttribute(string name) -> void StreamJsonRpc.JsonRpcMethodAttribute.Name.get -> string StreamJsonRpc.JsonRpc.AddLocalRpcMethod(string rpcMethodName, System.Delegate handler) -> void StreamJsonRpc.JsonRpc.AddLocalRpcMethod(string rpcMethodName, System.Reflection.MethodInfo handler, object target) -> void diff --git a/src/StreamJsonRpc/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/PublicAPI.Unshipped.txt index 00389f57..6152ead5 100644 --- a/src/StreamJsonRpc/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/PublicAPI.Unshipped.txt @@ -16,6 +16,7 @@ StreamJsonRpc.JsonRpc.Attach(System.Type interfaceType) -> object StreamJsonRpc.JsonRpc.Attach(System.Type interfaceType, StreamJsonRpc.JsonRpcProxyOptions options) -> object StreamJsonRpc.JsonRpc.TraceEvents.ProgressNotificationError = 16 -> StreamJsonRpc.JsonRpc.TraceEvents StreamJsonRpc.JsonRpcMethodAttribute.UseSingleObjectParameterDeserialization.get -> bool +StreamJsonRpc.JsonRpcMethodAttribute.UseSingleObjectParameterDeserialization.set -> void StreamJsonRpc.Reflection.MessageFormatterDuplexPipeTracker StreamJsonRpc.Reflection.MessageFormatterDuplexPipeTracker.Dispose() -> void StreamJsonRpc.Reflection.MessageFormatterDuplexPipeTracker.GetPipe(int? token) -> System.IO.Pipelines.IDuplexPipe diff --git a/src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs b/src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs index 716003d6..5e7c401e 100644 --- a/src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs +++ b/src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs @@ -27,13 +27,11 @@ public class JsonRpcMethodAttribute : Attribute /// Initializes a new instance of the class. /// /// Replacement name of a method. - /// Indicates whether the method supports deserialization for single object parameter. - public JsonRpcMethodAttribute(string name, bool useSingleObjectParameterDeserialization = false) + public JsonRpcMethodAttribute(string name) { Requires.NotNullOrWhiteSpace(name, nameof(name)); this.Name = name; - this.UseSingleObjectParameterDeserialization = useSingleObjectParameterDeserialization; } /// @@ -42,8 +40,8 @@ public JsonRpcMethodAttribute(string name, bool useSingleObjectParameterDeserial public string Name { get; } /// - /// Gets a value indicating whether the method supports deserialization for sinlge object parameter. + /// Gets or sets a value indicating whether JSON-RPC named arguments should all be deserialized into this method's first parameter. /// - public bool UseSingleObjectParameterDeserialization { get; } + public bool UseSingleObjectParameterDeserialization { get; set; } } } From 31569df768c63022bad74e8ce2219bbd7ea91fe4 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 2 Nov 2019 07:47:00 -0600 Subject: [PATCH 6/8] Add JsonRpc.GetJsonRpcMethodAttribute method This generalizes the new method on JsonRpc and makes it public. --- src/StreamJsonRpc/JsonMessageFormatter.cs | 5 +- src/StreamJsonRpc/JsonRpc.cs | 57 +++++++++---------- src/StreamJsonRpc/PublicAPI.Unshipped.txt | 1 + .../Reflection/MethodSignature.cs | 23 +++++++- .../Reflection/MethodSignatureAndTarget.cs | 4 +- 5 files changed, 56 insertions(+), 34 deletions(-) diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index 9bb95b1a..ca186577 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -611,8 +611,9 @@ public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan property in this.NamedArguments) diff --git a/src/StreamJsonRpc/JsonRpc.cs b/src/StreamJsonRpc/JsonRpc.cs index 2918e648..22c7d458 100644 --- a/src/StreamJsonRpc/JsonRpc.cs +++ b/src/StreamJsonRpc/JsonRpc.cs @@ -746,7 +746,7 @@ public void AddLocalRpcMethod(string rpcMethodName, MethodInfo handler, object t this.ThrowIfConfigurationLocked(); lock (this.syncObject) { - var methodTarget = new MethodSignatureAndTarget(handler, target); + var methodTarget = new MethodSignatureAndTarget(handler, target, attribute: null); this.TraceLocalMethodAdded(rpcMethodName, methodTarget); if (this.targetRequestMethodToClrMethodMap.TryGetValue(rpcMethodName, out List existingList)) { @@ -764,6 +764,30 @@ public void AddLocalRpcMethod(string rpcMethodName, MethodInfo handler, object t } } + /// + /// Gets the for a previously discovered RPC method, if there is one. + /// + /// The name of the method for which the attribute is sought. + /// + /// The list of parameters found on the method, as they may be given to . + /// Note this list may omit some special parameters such as a trailing . + /// + public JsonRpcMethodAttribute GetJsonRpcMethodAttribute(string methodName, ReadOnlySpan parameters) + { + if (this.targetRequestMethodToClrMethodMap.TryGetValue(methodName, out List existingList)) + { + foreach (MethodSignatureAndTarget entry in existingList) + { + if (entry.Signature.MatchesParametersExcludingCancellationToken(parameters)) + { + return entry.Signature.Attribute; + } + } + } + + return null; + } + /// /// Starts listening to incoming messages. /// @@ -1042,32 +1066,6 @@ public void Dispose() GC.SuppressFinalize(this); } - /// - /// Tries to obtain the info of a method. - /// - internal bool MethodSupportsSingleParameterObjectDeserialization(string methodName, Type parameterType) - { - // Get method definitions - if (this.targetRequestMethodToClrMethodMap.TryGetValue(methodName, out List existingList)) - { - // Check if there is one definition with one parameter of the expected type. - if (existingList.Any(m => m.Signature.TotalParamCountExcludingCancellationToken == 1 && m.Signature.Parameters[0].ParameterType == parameterType)) - { - // Try to obtain the JsonRpcMethod attribute and check if the method supports single parameter object deserialization - MethodSignatureAndTarget methodSignatureAndTarget = existingList.First(m => m.Signature.TotalParamCountExcludingCancellationToken == 1 && m.Signature.Parameters[0].ParameterType == parameterType); - var mapping = new JsonRpc.MethodNameMap(methodSignatureAndTarget.Target.GetType().GetTypeInfo()); - JsonRpcMethodAttribute rpcMethod = mapping.FindAttribute(methodSignatureAndTarget.Signature.MethodInfo); - - if (rpcMethod != null && rpcMethod.UseSingleObjectParameterDeserialization) - { - return true; - } - } - } - - return false; - } - /// /// Disposes managed and native resources held by this instance. /// @@ -1301,8 +1299,10 @@ private static Dictionary> 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; @@ -1312,7 +1312,6 @@ private static Dictionary> 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); diff --git a/src/StreamJsonRpc/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/PublicAPI.Unshipped.txt index 06353bfc..1d1e7cb4 100644 --- a/src/StreamJsonRpc/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +StreamJsonRpc.JsonRpc.GetJsonRpcMethodAttribute(string methodName, System.ReadOnlySpan parameters) -> StreamJsonRpc.JsonRpcMethodAttribute StreamJsonRpc.JsonRpc.InvokeCoreAsync(StreamJsonRpc.RequestId id, string targetName, System.Collections.Generic.IReadOnlyList arguments, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task StreamJsonRpc.JsonRpc.InvokeCoreAsync(StreamJsonRpc.RequestId id, string targetName, System.Collections.Generic.IReadOnlyList arguments, System.Threading.CancellationToken cancellationToken, bool isParameterObject) -> System.Threading.Tasks.Task StreamJsonRpc.JsonRpcMethodAttribute.UseSingleObjectParameterDeserialization.get -> bool diff --git a/src/StreamJsonRpc/Reflection/MethodSignature.cs b/src/StreamJsonRpc/Reflection/MethodSignature.cs index 0c2be168..ed576f61 100644 --- a/src/StreamJsonRpc/Reflection/MethodSignature.cs +++ b/src/StreamJsonRpc/Reflection/MethodSignature.cs @@ -22,14 +22,17 @@ internal sealed class MethodSignature : IEquatable /// 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; @@ -113,6 +116,24 @@ public override string ToString() return this.DebuggerDisplay; } + internal bool MatchesParametersExcludingCancellationToken(ReadOnlySpan 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; } } diff --git a/src/StreamJsonRpc/Reflection/MethodSignatureAndTarget.cs b/src/StreamJsonRpc/Reflection/MethodSignatureAndTarget.cs index bca2e4c7..79092022 100644 --- a/src/StreamJsonRpc/Reflection/MethodSignatureAndTarget.cs +++ b/src/StreamJsonRpc/Reflection/MethodSignatureAndTarget.cs @@ -13,11 +13,11 @@ namespace StreamJsonRpc [DebuggerDisplay("{DebuggerDisplay}")] internal struct MethodSignatureAndTarget : IEquatable { - public MethodSignatureAndTarget(MethodInfo method, object target) + public MethodSignatureAndTarget(MethodInfo method, object target, JsonRpcMethodAttribute attribute) { Requires.NotNull(method, nameof(method)); - this.Signature = new MethodSignature(method); + this.Signature = new MethodSignature(method, attribute); this.Target = target; } From 1771f52e7250730ea11f044c307cd35955d58582 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 2 Nov 2019 08:06:32 -0600 Subject: [PATCH 7/8] Add JsonRpc.AddLocalRpcMethod overload that accepts JsonRpcMethodAttribute This gives per-method callers the ability to customize how their RPC method is invoked using all the power of the attribute. It also adds a default constructor to the attribute so its more generally useful now that it has more functionality. --- src/StreamJsonRpc.Tests/JsonRpcTests.cs | 22 ++++++++++++++++--- src/StreamJsonRpc/JsonRpc.cs | 21 +++++++++++++++--- src/StreamJsonRpc/PublicAPI.Unshipped.txt | 2 ++ .../Reflection/JsonRpcMethodAttribute.cs | 10 ++++++++- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/StreamJsonRpc.Tests/JsonRpcTests.cs b/src/StreamJsonRpc.Tests/JsonRpcTests.cs index edf3c891..136c3833 100644 --- a/src/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/src/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -977,7 +977,7 @@ public async Task InvokeWithSingleObjectParameter_SendingExpectedObjectAndCancel { Skip.If(this.clientMessageFormatter is MessagePackFormatter, "Single object deserialization is not supported for MessagePack"); - int sum = await this.clientRpc.InvokeWithParameterObjectAsync("test/MethodWithSingleObjectParameterAndCancellationToken", new XAndYFields { x = 2, y = 5 }, this.TimeoutToken); + int sum = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithSingleObjectParameterAndCancellationToken), new XAndYFields { x = 2, y = 5 }, this.TimeoutToken); Assert.Equal(7, sum); } @@ -1306,6 +1306,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("biz.bar", "foo"); + Assert.Equal("foo!", result); + } + [Fact] public void AddLocalRpcMethod_String_MethodInfo_Object_NonNullTargetForStaticMethod() { @@ -1726,7 +1742,7 @@ public static int MethodWithSingleObjectParameterVAndW(VAndWFields fields) return fields.v + fields.w; } - [JsonRpcMethod("test/MethodWithSingleObjectParameterAndCancellationToken", UseSingleObjectParameterDeserialization = true)] + [JsonRpcMethod(UseSingleObjectParameterDeserialization = true)] public static int MethodWithSingleObjectParameterAndCancellationToken(XAndYFields fields, CancellationToken token) { return fields.x + fields.y; @@ -2025,7 +2041,7 @@ internal void InternalMethod() this.ServerMethodReached.Set(); } - [JsonRpcMethod("InternalMethodWithAttribute")] + [JsonRpcMethod] internal void InternalMethodWithAttribute() { this.ServerMethodReached.Set(); diff --git a/src/StreamJsonRpc/JsonRpc.cs b/src/StreamJsonRpc/JsonRpc.cs index 22c7d458..b01590ad 100644 --- a/src/StreamJsonRpc/JsonRpc.cs +++ b/src/StreamJsonRpc/JsonRpc.cs @@ -737,16 +737,31 @@ public void AddLocalRpcMethod(string rpcMethodName, Delegate handler) /// This method may accept parameters from the incoming JSON-RPC message. /// /// An instance of the type that defines which should handle the invocation. - 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)); + + /// + /// Adds a handler for an RPC method with a given name. + /// + /// + /// The method or delegate to invoke when a matching RPC message arrives. + /// This method may accept parameters from the incoming JSON-RPC message. + /// + /// An instance of the type that defines which should handle the invocation. + /// + /// A description for how this method should be treated. + /// It need not be an attribute that was actually applied to . + /// An attribute will *not* be discovered via reflection on the , even if this value is null. + /// + 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, attribute: null); + var methodTarget = new MethodSignatureAndTarget(handler, target, methodRpcSettings); this.TraceLocalMethodAdded(rpcMethodName, methodTarget); if (this.targetRequestMethodToClrMethodMap.TryGetValue(rpcMethodName, out List existingList)) { diff --git a/src/StreamJsonRpc/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/PublicAPI.Unshipped.txt index 1d1e7cb4..3f4cf431 100644 --- a/src/StreamJsonRpc/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/PublicAPI.Unshipped.txt @@ -1,6 +1,8 @@ +StreamJsonRpc.JsonRpc.AddLocalRpcMethod(System.Reflection.MethodInfo handler, object target, StreamJsonRpc.JsonRpcMethodAttribute methodRpcSettings) -> void StreamJsonRpc.JsonRpc.GetJsonRpcMethodAttribute(string methodName, System.ReadOnlySpan parameters) -> StreamJsonRpc.JsonRpcMethodAttribute StreamJsonRpc.JsonRpc.InvokeCoreAsync(StreamJsonRpc.RequestId id, string targetName, System.Collections.Generic.IReadOnlyList arguments, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task StreamJsonRpc.JsonRpc.InvokeCoreAsync(StreamJsonRpc.RequestId id, string targetName, System.Collections.Generic.IReadOnlyList arguments, System.Threading.CancellationToken cancellationToken, bool isParameterObject) -> System.Threading.Tasks.Task +StreamJsonRpc.JsonRpcMethodAttribute.JsonRpcMethodAttribute() -> void StreamJsonRpc.JsonRpcMethodAttribute.UseSingleObjectParameterDeserialization.get -> bool StreamJsonRpc.JsonRpcMethodAttribute.UseSingleObjectParameterDeserialization.set -> void StreamJsonRpc.Protocol.JsonRpcError.RequestId.get -> StreamJsonRpc.RequestId diff --git a/src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs b/src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs index 5e7c401e..d999299e 100644 --- a/src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs +++ b/src/StreamJsonRpc/Reflection/JsonRpcMethodAttribute.cs @@ -35,8 +35,16 @@ public JsonRpcMethodAttribute(string name) } /// - /// Gets the replacement name of a method. + /// Initializes a new instance of the class. /// + public JsonRpcMethodAttribute() + { + } + + /// + /// Gets the public RPC name by which this method will be invoked. + /// + /// May be null if the method's name has not been overridden. public string Name { get; } /// From 8753b1e337184b7526d27c1672abeb799e8f628c Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 2 Nov 2019 08:11:21 -0600 Subject: [PATCH 8/8] Add test for attributing a single arg method on the interface --- src/StreamJsonRpc.Tests/JsonRpcTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/StreamJsonRpc.Tests/JsonRpcTests.cs b/src/StreamJsonRpc.Tests/JsonRpcTests.cs index 136c3833..f1f8f4af 100644 --- a/src/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/src/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -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] @@ -981,6 +984,15 @@ public async Task InvokeWithSingleObjectParameter_SendingExpectedObjectAndCancel 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(nameof(IServer.InstanceMethodWithSingleObjectParameterAndCancellationToken), new XAndYFields { x = 2, y = 5 }, this.TimeoutToken); + Assert.Equal(7, sum); + } + [SkippableFact] public async Task InvokeWithSingleObjectParameter_SendingWithProgressProperty() { @@ -1793,6 +1805,11 @@ public static int MethodWithInvalidProgressParameter(Progress 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);