From 580d97fe1f0883cc714dd75f4d7d2e18dbfc36c4 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 17 Apr 2023 21:21:16 -0600 Subject: [PATCH 01/35] Generalize the JSON tests that can be --- .../JsonRpcJsonHeadersTests.cs | 85 ------------------- .../JsonRpcMessagePackLengthTests.cs | 4 +- test/StreamJsonRpc.Tests/JsonRpcTests.cs | 72 ++++++++++++++++ 3 files changed, 74 insertions(+), 87 deletions(-) diff --git a/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs b/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs index d028e3cf..fb43e842 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs @@ -44,13 +44,6 @@ public async Task CustomJsonConvertersAreNotAppliedToBaseMessage() Assert.Equal("YSE=", result); // a! } - [Fact] - public async Task CanInvokeServerMethodWithParameterPassedAsObject() - { - string result1 = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.TestParameter), new { test = "test" }); - Assert.Equal("object {" + Environment.NewLine + " \"test\": \"test\"" + Environment.NewLine + "}", result1); - } - [Fact] public async Task InvokeWithParameterObjectAsync_AndCancel() { @@ -87,33 +80,6 @@ public async Task InvokeWithParameterObjectAsync_AndComplete() } } - [Fact] - public async Task InvokeWithParameterObject_WithRenamingAttributes() - { - var param = new ParamsObjectWithCustomNames { TheArgument = "hello" }; - string result = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.ServerMethod), param, this.TimeoutToken); - Assert.Equal(param.TheArgument + "!", result); - } - - [Fact] - public async Task CanInvokeServerMethodWithParameterPassedAsArray() - { - string result1 = await this.clientRpc.InvokeAsync(nameof(Server.TestParameter), "test"); - Assert.Equal("object test", result1); - } - - [Fact] - public async Task InvokeWithCancellationAsync_AndCancel() - { - using (var cts = new CancellationTokenSource()) - { - var invokeTask = this.clientRpc.InvokeWithCancellationAsync(nameof(Server.AsyncMethodWithJTokenAndCancellation), new[] { "a" }, cts.Token); - await Task.WhenAny(invokeTask, this.server.ServerMethodReached.WaitAsync(this.TimeoutToken)); - cts.Cancel(); - await Assert.ThrowsAnyAsync(() => invokeTask); - } - } - [Fact] public async Task InvokeWithCancellationAsync_AndComplete() { @@ -126,24 +92,6 @@ public async Task InvokeWithCancellationAsync_AndComplete() } } - [Fact] - public async Task CanPassAndCallPrivateMethodsObjects() - { - var result = await this.clientRpc.InvokeAsync(nameof(Server.MethodThatAcceptsFoo), new Foo { Bar = "bar", Bazz = 1000 }); - Assert.NotNull(result); - Assert.Equal("bar!", result.Bar); - Assert.Equal(1001, result.Bazz); - - // anonymous types are not supported when TypeHandling is set to "Object" or "Auto". - if (!this.IsTypeNameHandlingEnabled) - { - result = await this.clientRpc.InvokeAsync(nameof(Server.MethodThatAcceptsFoo), new { Bar = "bar", Bazz = 1000 }); - Assert.NotNull(result); - Assert.Equal("bar!", result.Bar); - Assert.Equal(1001, result.Bazz); - } - } - [Fact] public async Task Completion_FaultsOnFatalError() { @@ -155,32 +103,6 @@ public async Task Completion_FaultsOnFatalError() Assert.Same(completion, this.serverRpc.Completion); } - [Fact] - public async Task ExceptionControllingErrorData() - { - var exception = await Assert.ThrowsAsync(() => this.clientRpc.InvokeAsync(nameof(Server.ThrowLocalRpcException))); - - // C# dynamic is infamously unstable. If this test ends up being unstable, yet the dump clearly shows the fields are there even though the exception claims it isn't, - // that's consistent with the instability I've seen before. Switching to using JToken APIs will fix the instability, but it relies on using the JsonMessageFormatter. - dynamic? data = exception.ErrorData; - dynamic myCustomData = data!.myCustomData; - string actual = myCustomData; - Assert.Equal("hi", actual); - } - - [Fact] - public async Task CanPassExceptionFromServer_ErrorData() - { - RemoteInvocationException exception = await Assert.ThrowsAnyAsync(() => this.clientRpc.InvokeAsync(nameof(Server.MethodThatThrowsUnauthorizedAccessException))); - Assert.Equal((int)JsonRpcErrorCode.InvocationError, exception.ErrorCode); - - var errorDataJToken = (JToken?)exception.ErrorData; - Assert.NotNull(errorDataJToken); - var errorData = errorDataJToken!.ToObject(new JsonSerializer()); - Assert.NotNull(errorData?.StackTrace); - Assert.StrictEqual(COR_E_UNAUTHORIZEDACCESS, errorData?.HResult); - } - protected override void InitializeFormattersAndHandlers(bool controlledFlushingClient) { this.clientMessageFormatter = new JsonMessageFormatter @@ -212,13 +134,6 @@ protected override void InitializeFormattersAndHandlers(bool controlledFlushingC : new HeaderDelimitedMessageHandler(this.clientStream, this.clientStream, this.clientMessageFormatter); } - [DataContract] - public class ParamsObjectWithCustomNames - { - [DataMember(Name = "argument")] - public string? TheArgument { get; set; } - } - protected class UnserializableTypeConverter : JsonConverter { public override bool CanConvert(Type objectType) => objectType == typeof(CustomSerializedType); diff --git a/test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs b/test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs index a8b8f49d..f6ce5fd3 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs @@ -34,7 +34,7 @@ internal interface IMessagePackServer } [Fact] - public async Task CanPassAndCallPrivateMethodsObjects() + public override async Task CanPassAndCallPrivateMethodsObjects() { var result = await this.clientRpc.InvokeAsync(nameof(Server.MethodThatAcceptsFoo), new Foo { Bar = "bar", Bazz = 1000 }); Assert.NotNull(result); @@ -55,7 +55,7 @@ public async Task ExceptionControllingErrorData() } [Fact] - public async Task CanPassExceptionFromServer_ErrorData() + public override async Task CanPassExceptionFromServer_ErrorData() { RemoteInvocationException exception = await Assert.ThrowsAnyAsync(() => this.clientRpc.InvokeAsync(nameof(Server.MethodThatThrowsUnauthorizedAccessException))); Assert.Equal((int)JsonRpcErrorCode.InvocationError, exception.ErrorCode); diff --git a/test/StreamJsonRpc.Tests/JsonRpcTests.cs b/test/StreamJsonRpc.Tests/JsonRpcTests.cs index 4cb36bc2..373288a0 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -2907,6 +2907,71 @@ public void JoinableTaskFactory_IntegrationClientSideOnly() }); } + [Fact] + public async Task CanInvokeServerMethodWithParameterPassedAsObject() + { + string result1 = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.TestParameter), new { test = "test" }); + Assert.Equal("object {" + Environment.NewLine + " \"test\": \"test\"" + Environment.NewLine + "}", result1); + } + + [Fact] + public async Task InvokeWithParameterObject_WithRenamingAttributes() + { + var param = new ParamsObjectWithCustomNames { TheArgument = "hello" }; + string result = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.ServerMethod), param, this.TimeoutToken); + Assert.Equal(param.TheArgument + "!", result); + } + + [Fact] + public async Task CanInvokeServerMethodWithParameterPassedAsArray() + { + string result1 = await this.clientRpc.InvokeAsync(nameof(Server.TestParameter), "test"); + Assert.Equal("object test", result1); + } + + [Fact] + public async Task InvokeWithCancellationAsync_AndCancel() + { + using (var cts = new CancellationTokenSource()) + { + var invokeTask = this.clientRpc.InvokeWithCancellationAsync(nameof(Server.AsyncMethodWithJTokenAndCancellation), new[] { "a" }, cts.Token); + await Task.WhenAny(invokeTask, this.server.ServerMethodReached.WaitAsync(this.TimeoutToken)); + cts.Cancel(); + await Assert.ThrowsAnyAsync(() => invokeTask); + } + } + + [Fact] + public virtual async Task CanPassAndCallPrivateMethodsObjects() + { + var result = await this.clientRpc.InvokeAsync(nameof(Server.MethodThatAcceptsFoo), new Foo { Bar = "bar", Bazz = 1000 }); + Assert.NotNull(result); + Assert.Equal("bar!", result.Bar); + Assert.Equal(1001, result.Bazz); + + // anonymous types are not supported when TypeHandling is set to "Object" or "Auto". + if (!this.IsTypeNameHandlingEnabled) + { + result = await this.clientRpc.InvokeAsync(nameof(Server.MethodThatAcceptsFoo), new { Bar = "bar", Bazz = 1000 }); + Assert.NotNull(result); + Assert.Equal("bar!", result.Bar); + Assert.Equal(1001, result.Bazz); + } + } + + [Fact] + public virtual async Task CanPassExceptionFromServer_ErrorData() + { + RemoteInvocationException exception = await Assert.ThrowsAnyAsync(() => this.clientRpc.InvokeAsync(nameof(Server.MethodThatThrowsUnauthorizedAccessException))); + Assert.Equal((int)JsonRpcErrorCode.InvocationError, exception.ErrorCode); + + var errorDataJToken = (JToken?)exception.ErrorData; + Assert.NotNull(errorDataJToken); + var errorData = errorDataJToken!.ToObject(new JsonSerializer()); + Assert.NotNull(errorData?.StackTrace); + Assert.StrictEqual(COR_E_UNAUTHORIZEDACCESS, errorData?.HResult); + } + protected static Exception CreateExceptionToBeThrownByDeserializer() => new Exception("This exception is meant to be thrown."); protected override void Dispose(bool disposing) @@ -3662,6 +3727,13 @@ public ValueTask DisposeAsync() } } + [DataContract] + public class ParamsObjectWithCustomNames + { + [DataMember(Name = "argument")] + public string? TheArgument { get; set; } + } + public class VsThreadingAsyncDisposableServer : Microsoft.VisualStudio.Threading.IAsyncDisposable { internal bool IsDisposed { get; private set; } From a5f8362c0c81614e56fbbabe7625ae7f592b1a0c Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 17 Apr 2023 21:21:32 -0600 Subject: [PATCH 02/35] Early System.Text.Json formatter work --- Directory.Packages.props | 1 + src/StreamJsonRpc/StreamJsonRpc.csproj | 1 + src/StreamJsonRpc/SystemTextJsonFormatter.cs | 55 ++++++++++++++ .../JsonRpcSystemTextJsonHeadersTests.cs | 72 +++++++++++++++++++ .../StreamJsonRpc.Tests.csproj | 1 + 5 files changed, 130 insertions(+) create mode 100644 src/StreamJsonRpc/SystemTextJsonFormatter.cs create mode 100644 test/StreamJsonRpc.Tests/JsonRpcSystemTextJsonHeadersTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index fdf08c9d..22ca8693 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,6 +28,7 @@ + diff --git a/src/StreamJsonRpc/StreamJsonRpc.csproj b/src/StreamJsonRpc/StreamJsonRpc.csproj index e78ea693..ac61558c 100644 --- a/src/StreamJsonRpc/StreamJsonRpc.csproj +++ b/src/StreamJsonRpc/StreamJsonRpc.csproj @@ -26,6 +26,7 @@ + diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs new file mode 100644 index 00000000..de36e460 --- /dev/null +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Buffers; +using System.Text; +using System.Text.Json; +using StreamJsonRpc.Protocol; + +namespace StreamJsonRpc; + +public class SystemTextJsonFormatter : IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter +{ + private static JsonWriterOptions WriterOptions = new(); + private static JsonReaderOptions ReaderOptions = new(); + + private static JsonSerializerOptions SerializerOptions = new(); + + /// + /// UTF-8 encoding without a preamble. + /// + private static readonly Encoding DefaultEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + public Encoding Encoding + { + get => DefaultEncoding; + set => throw new NotSupportedException(); + } + + public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer) + { + throw new NotImplementedException(); + } + + public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding encoding) + { + if (encoding is not UTF8Encoding) + { + throw new NotSupportedException("Only our default encoding is supported."); + } + + Utf8JsonReader reader = new(contentBuffer, ReaderOptions); + return JsonSerializer.Deserialize(ref reader, SerializerOptions) ?? throw new Exception("Empty message"); + } + + public object GetJsonText(JsonRpcMessage message) + { + throw new NotImplementedException(); + } + + public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message) + { + using Utf8JsonWriter writer = new(bufferWriter, WriterOptions); + JsonSerializer.Serialize(writer, message, SerializerOptions); + } +} diff --git a/test/StreamJsonRpc.Tests/JsonRpcSystemTextJsonHeadersTests.cs b/test/StreamJsonRpc.Tests/JsonRpcSystemTextJsonHeadersTests.cs new file mode 100644 index 00000000..d601a3a9 --- /dev/null +++ b/test/StreamJsonRpc.Tests/JsonRpcSystemTextJsonHeadersTests.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.Serialization; +using System.Text; +using Microsoft.VisualStudio.Threading; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StreamJsonRpc; +using StreamJsonRpc.Protocol; +using Xunit; +using Xunit.Abstractions; + +public class JsonRpcSystemTextJsonHeadersTests : JsonRpcTests +{ + public JsonRpcSystemTextJsonHeadersTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override void InitializeFormattersAndHandlers(bool controlledFlushingClient) + { + this.clientMessageFormatter = new SystemTextJsonFormatter + { + }; + this.serverMessageFormatter = new SystemTextJsonFormatter + { + }; + + this.serverMessageHandler = new HeaderDelimitedMessageHandler(this.serverStream, this.serverStream, this.serverMessageFormatter); + this.clientMessageHandler = controlledFlushingClient + ? new DelayedFlushingHandler(this.clientStream, this.clientMessageFormatter) + : new HeaderDelimitedMessageHandler(this.clientStream, this.clientStream, this.clientMessageFormatter); + } + + protected class DelayedFlushingHandler : HeaderDelimitedMessageHandler, IControlledFlushHandler + { + public DelayedFlushingHandler(Stream stream, IJsonRpcMessageFormatter formatter) + : base(stream, formatter) + { + } + + public AsyncAutoResetEvent FlushEntered { get; } = new AsyncAutoResetEvent(); + + public AsyncManualResetEvent AllowFlushAsyncExit { get; } = new AsyncManualResetEvent(); + + protected override async ValueTask FlushAsync(CancellationToken cancellationToken) + { + this.FlushEntered.Set(); + await this.AllowFlushAsyncExit.WaitAsync(); + await base.FlushAsync(cancellationToken); + } + } + + private class StringBase64Converter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(string); + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + string decoded = Encoding.UTF8.GetString(Convert.FromBase64String((string)reader.Value!)); + return decoded; + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + var stringValue = (string?)value; + var encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(stringValue!)); + writer.WriteValue(encoded); + } + } +} diff --git a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj index cbf69b39..852368f6 100644 --- a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj +++ b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj @@ -13,6 +13,7 @@ + From 6cefbab1720c9b3efff073086933e67cffc6d517 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 18 Apr 2023 09:00:33 -0600 Subject: [PATCH 03/35] Implement most of the message envelope serialization --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 238 ++++++++++++++++++- 1 file changed, 234 insertions(+), 4 deletions(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index de36e460..653c03f2 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -4,16 +4,23 @@ using System.Buffers; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using StreamJsonRpc.Protocol; namespace StreamJsonRpc; -public class SystemTextJsonFormatter : IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter +public partial class SystemTextJsonFormatter : IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter { private static JsonWriterOptions WriterOptions = new(); + private static JsonReaderOptions ReaderOptions = new(); - private static JsonSerializerOptions SerializerOptions = new(); + private static JsonSerializerOptions SerializerOptions = new() { TypeInfoResolver = SourceGenerated.Default }; + + private static JsonSerializerOptions UserDataSerializerOptions = new(); + + private static JsonDocumentOptions DocumentOptions = new(); /// /// UTF-8 encoding without a preamble. @@ -39,7 +46,90 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding } Utf8JsonReader reader = new(contentBuffer, ReaderOptions); - return JsonSerializer.Deserialize(ref reader, SerializerOptions) ?? throw new Exception("Empty message"); + + if (!reader.Read()) + { + throw new Exception("Unexpected end of message."); + } + + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new Exception("Expected start of object."); + } + + JsonDocument document = JsonDocument.Parse(contentBuffer, DocumentOptions); + if (document.RootElement.ValueKind != JsonValueKind.Object) + { + throw new Exception("Expected a JSON object at the root of the message."); + } + + JsonRpcMessage returnValue; + if (document.RootElement.TryGetProperty(Utf8Strings.method, out JsonElement methodElement)) + { + JsonRpcRequest request = new() + { + RequestId = ReadRequestId(), + Method = methodElement.GetString(), + }; + returnValue = request; + } + else if (document.RootElement.TryGetProperty(Utf8Strings.result, out JsonElement resultElement)) + { + JsonRpcResult result = new() + { + RequestId = ReadRequestId(), + Result = null, // TODO + }; + returnValue = result; + } + else if (document.RootElement.TryGetProperty(Utf8Strings.error, out JsonElement errorElement)) + { + JsonRpcError error = new() + { + RequestId = ReadRequestId(), + Error = new JsonRpcError.ErrorDetail + { + Code = (JsonRpcErrorCode)errorElement.GetProperty(Utf8Strings.code).GetInt64(), + Message = errorElement.GetProperty(Utf8Strings.message).GetString(), + }, + }; + + returnValue = error; + } + else + { + throw new Exception("Expected a request, result, or error message."); + } + + if (document.RootElement.TryGetProperty(Utf8Strings.jsonrpc, out JsonElement jsonRpcElement)) + { + returnValue.Version = jsonRpcElement.ValueEquals(Utf8Strings.v2_0) ? "2.0" : (jsonRpcElement.GetString() ?? throw new Exception("Unexpected null value for jsonrpc property.")); + } + else + { + // Version 1.0 is implied when it is absent. + returnValue.Version = "1.0"; + } + + RequestId ReadRequestId() + { + if (document.RootElement.TryGetProperty(Utf8Strings.id, out JsonElement idElement)) + { + return idElement.ValueKind switch + { + JsonValueKind.Number => new RequestId(idElement.GetInt64()), + JsonValueKind.String => new RequestId(idElement.GetString()), + JsonValueKind.Null => new RequestId(null), + _ => throw new Exception("Unexpected value kind for id property: " + idElement.ValueKind), + }; + } + else + { + return RequestId.NotSpecified; + } + } + + return returnValue; } public object GetJsonText(JsonRpcMessage message) @@ -50,6 +140,146 @@ public object GetJsonText(JsonRpcMessage message) public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message) { using Utf8JsonWriter writer = new(bufferWriter, WriterOptions); - JsonSerializer.Serialize(writer, message, SerializerOptions); + writer.WriteStartObject(); + WriteVersion(); + switch (message) + { + case JsonRpcRequest request: + WriteId(request.RequestId); + writer.WriteString(Utf8Strings.method, request.Method); + WriteArguments(request); + writer.WriteEndObject(); + break; + case JsonRpcResult result: + WriteId(result.RequestId); + WriteResult(result); + writer.WriteEndObject(); + break; + case JsonRpcError error: + WriteId(error.RequestId); + WriteError(error); + writer.WriteEndObject(); + break; + default: + throw new ArgumentException("Unknown message type: " + message.GetType().Name, nameof(message)); + } + + void WriteVersion() + { + switch (message.Version) + { + case "1.0": + // The 1.0 protocol didn't include the version property at all. + break; + case "2.0": + writer.WriteString(Utf8Strings.jsonrpc, Utf8Strings.v2_0); + break; + default: + writer.WriteString(Utf8Strings.jsonrpc, message.Version); + break; + } + } + + void WriteId(RequestId requestId) + { + if (requestId.Number is long idNumber) + { + writer.WriteNumber(Utf8Strings.id, idNumber); + } + else if (requestId.String is string idString) + { + writer.WriteString(Utf8Strings.id, idString); + } + else + { + writer.WriteNull(Utf8Strings.id); + } + } + + void WriteArguments(JsonRpcRequest request) + { + if (request.ArgumentsList is not null) + { + writer.WriteStartArray(Utf8Strings.@params); + for (int i = 0; i < request.ArgumentsList.Count; i++) + { + WriteUserData(request.ArgumentsList[i]); + } + + writer.WriteEndArray(); + } + else if (request.NamedArguments is not null) + { + writer.WriteStartObject(Utf8Strings.@params); + foreach (KeyValuePair argument in request.NamedArguments) + { + writer.WritePropertyName(argument.Key); + WriteUserData(argument.Value); + } + + writer.WriteEndObject(); + } + } + + void WriteResult(JsonRpcResult result) + { + writer.WritePropertyName(Utf8Strings.result); + WriteUserData(result.Result); + } + + void WriteError(JsonRpcError error) + { + if (error.Error is null) + { + throw new ArgumentException($"{nameof(error.Error)} property must be set.", nameof(message)); + } + + writer.WriteStartObject(Utf8Strings.error); + writer.WriteNumber(Utf8Strings.code, (int)error.Error.Code); + writer.WriteString(Utf8Strings.message, error.Error.Message); + if (error.Error.Data is not null) + { + writer.WritePropertyName(Utf8Strings.data); + WriteUserData(error.Error.Data); + } + + writer.WriteEndObject(); + } + + void WriteUserData(object? value) + { + JsonSerializer.Serialize(writer, value, UserDataSerializerOptions); + } + } + + [JsonSerializable(typeof(JsonRpcMessage))] + [JsonSerializable(typeof(JsonRpcRequest))] + private partial class SourceGenerated : JsonSerializerContext + { + } + + private static class Utf8Strings + { +#pragma warning disable SA1300 // Element should begin with upper-case letter + internal static ReadOnlySpan jsonrpc => "jsonrpc"u8; + + internal static ReadOnlySpan v2_0 => "2.0"u8; + + internal static ReadOnlySpan id => "id"u8; + + internal static ReadOnlySpan method => "method"u8; + + internal static ReadOnlySpan @params => "params"u8; + + internal static ReadOnlySpan result => "result"u8; + + internal static ReadOnlySpan error => "error"u8; + + internal static ReadOnlySpan code => "code"u8; + + internal static ReadOnlySpan message => "message"u8; + + internal static ReadOnlySpan data => "data"u8; +#pragma warning restore SA1300 // Element should begin with upper-case letter } } From 623383341052ed31a3b298473ac0715ad9ebb2a9 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 18 Apr 2023 09:08:50 -0600 Subject: [PATCH 04/35] Work on warnings --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 24 +++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 653c03f2..d60f92f1 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -5,39 +5,41 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; using StreamJsonRpc.Protocol; namespace StreamJsonRpc; public partial class SystemTextJsonFormatter : IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter { - private static JsonWriterOptions WriterOptions = new(); + private static readonly JsonWriterOptions WriterOptions = new() { }; - private static JsonReaderOptions ReaderOptions = new(); + private static readonly JsonReaderOptions ReaderOptions = new() { }; - private static JsonSerializerOptions SerializerOptions = new() { TypeInfoResolver = SourceGenerated.Default }; + ////private static readonly JsonSerializerOptions SerializerOptions = new() { TypeInfoResolver = SourceGenerated.Default }; - private static JsonSerializerOptions UserDataSerializerOptions = new(); + private static readonly JsonSerializerOptions UserDataSerializerOptions = new(); - private static JsonDocumentOptions DocumentOptions = new(); + private static readonly JsonDocumentOptions DocumentOptions = new() { }; /// /// UTF-8 encoding without a preamble. /// private static readonly Encoding DefaultEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + /// public Encoding Encoding { get => DefaultEncoding; set => throw new NotSupportedException(); } + /// public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer) { throw new NotImplementedException(); } + /// public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding encoding) { if (encoding is not UTF8Encoding) @@ -252,11 +254,11 @@ void WriteUserData(object? value) } } - [JsonSerializable(typeof(JsonRpcMessage))] - [JsonSerializable(typeof(JsonRpcRequest))] - private partial class SourceGenerated : JsonSerializerContext - { - } + ////[JsonSerializable(typeof(JsonRpcMessage))] + ////[JsonSerializable(typeof(JsonRpcRequest))] + ////private partial class SourceGenerated : JsonSerializerContext + ////{ + ////} private static class Utf8Strings { From 5f2f13ac15415c5a894e2e65a3a3d99ccdfa2727 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 18 Apr 2023 11:27:06 -0600 Subject: [PATCH 05/35] Get most basic scenarios working The CanCallAsyncMethod test actually passes. --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 112 ++++++++++++++++-- .../netstandard2.0/PublicAPI.Unshipped.txt | 8 ++ .../netstandard2.1/PublicAPI.Unshipped.txt | 8 ++ 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index d60f92f1..8f4335b6 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -2,13 +2,17 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Buffers; +using System.Runtime.ExceptionServices; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; using StreamJsonRpc.Protocol; +using StreamJsonRpc.Reflection; namespace StreamJsonRpc; +/// +/// A formatter that emits UTF-8 encoded JSON where user data should be serializable via the . +/// public partial class SystemTextJsonFormatter : IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter { private static readonly JsonWriterOptions WriterOptions = new() { }; @@ -34,10 +38,7 @@ public Encoding Encoding } /// - public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer) - { - throw new NotImplementedException(); - } + public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer) => this.Deserialize(contentBuffer, this.Encoding); /// public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding encoding) @@ -72,6 +73,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding { RequestId = ReadRequestId(), Method = methodElement.GetString(), + JsonArguments = document.RootElement.TryGetProperty(Utf8Strings.@params, out JsonElement paramsElement) ? paramsElement : null, }; returnValue = request; } @@ -80,7 +82,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding JsonRpcResult result = new() { RequestId = ReadRequestId(), - Result = null, // TODO + JsonResult = resultElement, }; returnValue = result; } @@ -134,11 +136,13 @@ RequestId ReadRequestId() return returnValue; } + /// public object GetJsonText(JsonRpcMessage message) { throw new NotImplementedException(); } + /// public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message) { using Utf8JsonWriter writer = new(bufferWriter, WriterOptions); @@ -146,13 +150,13 @@ public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message) WriteVersion(); switch (message) { - case JsonRpcRequest request: + case Protocol.JsonRpcRequest request: WriteId(request.RequestId); writer.WriteString(Utf8Strings.method, request.Method); WriteArguments(request); writer.WriteEndObject(); break; - case JsonRpcResult result: + case Protocol.JsonRpcResult result: WriteId(result.RequestId); WriteResult(result); writer.WriteEndObject(); @@ -198,7 +202,7 @@ void WriteId(RequestId requestId) } } - void WriteArguments(JsonRpcRequest request) + void WriteArguments(Protocol.JsonRpcRequest request) { if (request.ArgumentsList is not null) { @@ -223,7 +227,7 @@ void WriteArguments(JsonRpcRequest request) } } - void WriteResult(JsonRpcResult result) + void WriteResult(Protocol.JsonRpcResult result) { writer.WritePropertyName(Utf8Strings.result); WriteUserData(result.Result); @@ -284,4 +288,92 @@ private static class Utf8Strings internal static ReadOnlySpan data => "data"u8; #pragma warning restore SA1300 // Element should begin with upper-case letter } + + private class JsonRpcRequest : Protocol.JsonRpcRequest, IJsonRpcMessageBufferManager + { + internal JsonElement? JsonArguments { get; set; } + + public void DeserializationComplete(JsonRpcMessage message) + { + Assumes.True(message == this); + + // Clear references to buffers that we are no longer entitled to. + this.JsonArguments = null; + } + + public override bool TryGetArgumentByNameOrIndex(string? name, int position, Type? typeHint, out object? value) + { + JsonElement? valueElement = null; + switch (this.JsonArguments?.ValueKind) + { + case JsonValueKind.Object when name is not null: + if (this.JsonArguments.Value.TryGetProperty(name, out JsonElement propertyValue)) + { + valueElement = propertyValue; + } + + break; + case JsonValueKind.Array: + int elementIndex = 0; + foreach (JsonElement arrayElement in this.JsonArguments.Value.EnumerateArray()) + { + if (elementIndex++ == position) + { + valueElement = arrayElement; + break; + } + } + + break; + default: + throw new Exception("Unexpected value kind for arguments: " + this.JsonArguments?.ValueKind ?? "null"); + } + + value = valueElement?.Deserialize(typeHint ?? typeof(object)); + return valueElement.HasValue; + } + } + + private class JsonRpcResult : Protocol.JsonRpcResult, IJsonRpcMessageBufferManager + { + private Exception? resultDeserializationException; + + internal JsonElement? JsonResult { get; set; } + + public void DeserializationComplete(JsonRpcMessage message) + { + Assumes.True(message == this); + + // Clear references to buffers that we are no longer entitled to. + this.JsonResult = null; + } + + public override T GetResult() + { + if (this.resultDeserializationException is not null) + { + ExceptionDispatchInfo.Capture(this.resultDeserializationException).Throw(); + } + + return this.JsonResult is null + ? (T)this.Result! + : this.JsonResult.Value.Deserialize()!; + } + + protected internal override void SetExpectedResultType(Type resultType) + { + Verify.Operation(this.JsonResult is not null, "Result is no longer available or has already been deserialized."); + + try + { + this.Result = this.JsonResult.Value.Deserialize(resultType); + this.JsonResult = default; + } + catch (Exception ex) + { + // This was a best effort anyway. We'll throw again later at a more convenient time for JsonRpc. + this.resultDeserializationException = ex; + } + } + } } diff --git a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt index e69de29b..51f671c8 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,8 @@ +StreamJsonRpc.SystemTextJsonFormatter +StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequence contentBuffer) -> StreamJsonRpc.Protocol.JsonRpcMessage! +StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequence contentBuffer, System.Text.Encoding! encoding) -> StreamJsonRpc.Protocol.JsonRpcMessage! +StreamJsonRpc.SystemTextJsonFormatter.Encoding.get -> System.Text.Encoding! +StreamJsonRpc.SystemTextJsonFormatter.Encoding.set -> void +StreamJsonRpc.SystemTextJsonFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> object! +StreamJsonRpc.SystemTextJsonFormatter.Serialize(System.Buffers.IBufferWriter! bufferWriter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void +StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void \ No newline at end of file diff --git a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt index e69de29b..51f671c8 100644 --- a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt @@ -0,0 +1,8 @@ +StreamJsonRpc.SystemTextJsonFormatter +StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequence contentBuffer) -> StreamJsonRpc.Protocol.JsonRpcMessage! +StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequence contentBuffer, System.Text.Encoding! encoding) -> StreamJsonRpc.Protocol.JsonRpcMessage! +StreamJsonRpc.SystemTextJsonFormatter.Encoding.get -> System.Text.Encoding! +StreamJsonRpc.SystemTextJsonFormatter.Encoding.set -> void +StreamJsonRpc.SystemTextJsonFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> object! +StreamJsonRpc.SystemTextJsonFormatter.Serialize(System.Buffers.IBufferWriter! bufferWriter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void +StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void \ No newline at end of file From c754f1a0d4ed20a668ddcd7ca23709fded1b1594 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 18 Apr 2023 11:41:10 -0600 Subject: [PATCH 06/35] Use the user serializer for their own types --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 63 +++++++++---------- .../netstandard2.0/PublicAPI.Unshipped.txt | 2 + .../netstandard2.1/PublicAPI.Unshipped.txt | 2 + 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 8f4335b6..81b2827f 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -19,10 +19,6 @@ public partial class SystemTextJsonFormatter : IJsonRpcMessageFormatter, IJsonRp private static readonly JsonReaderOptions ReaderOptions = new() { }; - ////private static readonly JsonSerializerOptions SerializerOptions = new() { TypeInfoResolver = SourceGenerated.Default }; - - private static readonly JsonSerializerOptions UserDataSerializerOptions = new(); - private static readonly JsonDocumentOptions DocumentOptions = new() { }; /// @@ -37,6 +33,11 @@ public Encoding Encoding set => throw new NotSupportedException(); } + /// + /// Gets or sets the options to use when serializing and deserializing JSON containing user data. + /// + public JsonSerializerOptions JsonSerializerOptions { get; set; } = new(); + /// public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer) => this.Deserialize(contentBuffer, this.Encoding); @@ -48,28 +49,16 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding throw new NotSupportedException("Only our default encoding is supported."); } - Utf8JsonReader reader = new(contentBuffer, ReaderOptions); - - if (!reader.Read()) - { - throw new Exception("Unexpected end of message."); - } - - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new Exception("Expected start of object."); - } - JsonDocument document = JsonDocument.Parse(contentBuffer, DocumentOptions); if (document.RootElement.ValueKind != JsonValueKind.Object) { - throw new Exception("Expected a JSON object at the root of the message."); + throw new JsonException("Expected a JSON object at the root of the message."); } JsonRpcMessage returnValue; if (document.RootElement.TryGetProperty(Utf8Strings.method, out JsonElement methodElement)) { - JsonRpcRequest request = new() + JsonRpcRequest request = new(this) { RequestId = ReadRequestId(), Method = methodElement.GetString(), @@ -79,7 +68,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding } else if (document.RootElement.TryGetProperty(Utf8Strings.result, out JsonElement resultElement)) { - JsonRpcResult result = new() + JsonRpcResult result = new(this) { RequestId = ReadRequestId(), JsonResult = resultElement, @@ -102,12 +91,12 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding } else { - throw new Exception("Expected a request, result, or error message."); + throw new JsonException("Expected a request, result, or error message."); } if (document.RootElement.TryGetProperty(Utf8Strings.jsonrpc, out JsonElement jsonRpcElement)) { - returnValue.Version = jsonRpcElement.ValueEquals(Utf8Strings.v2_0) ? "2.0" : (jsonRpcElement.GetString() ?? throw new Exception("Unexpected null value for jsonrpc property.")); + returnValue.Version = jsonRpcElement.ValueEquals(Utf8Strings.v2_0) ? "2.0" : (jsonRpcElement.GetString() ?? throw new JsonException("Unexpected null value for jsonrpc property.")); } else { @@ -124,7 +113,7 @@ RequestId ReadRequestId() JsonValueKind.Number => new RequestId(idElement.GetInt64()), JsonValueKind.String => new RequestId(idElement.GetString()), JsonValueKind.Null => new RequestId(null), - _ => throw new Exception("Unexpected value kind for id property: " + idElement.ValueKind), + _ => throw new JsonException("Unexpected value kind for id property: " + idElement.ValueKind), }; } else @@ -254,16 +243,10 @@ void WriteError(JsonRpcError error) void WriteUserData(object? value) { - JsonSerializer.Serialize(writer, value, UserDataSerializerOptions); + JsonSerializer.Serialize(writer, value, this.JsonSerializerOptions); } } - ////[JsonSerializable(typeof(JsonRpcMessage))] - ////[JsonSerializable(typeof(JsonRpcRequest))] - ////private partial class SourceGenerated : JsonSerializerContext - ////{ - ////} - private static class Utf8Strings { #pragma warning disable SA1300 // Element should begin with upper-case letter @@ -291,6 +274,13 @@ private static class Utf8Strings private class JsonRpcRequest : Protocol.JsonRpcRequest, IJsonRpcMessageBufferManager { + private readonly SystemTextJsonFormatter formatter; + + internal JsonRpcRequest(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + internal JsonElement? JsonArguments { get; set; } public void DeserializationComplete(JsonRpcMessage message) @@ -326,18 +316,25 @@ public override bool TryGetArgumentByNameOrIndex(string? name, int position, Typ break; default: - throw new Exception("Unexpected value kind for arguments: " + this.JsonArguments?.ValueKind ?? "null"); + throw new JsonException("Unexpected value kind for arguments: " + this.JsonArguments?.ValueKind ?? "null"); } - value = valueElement?.Deserialize(typeHint ?? typeof(object)); + value = valueElement?.Deserialize(typeHint ?? typeof(object), this.formatter.JsonSerializerOptions); return valueElement.HasValue; } } private class JsonRpcResult : Protocol.JsonRpcResult, IJsonRpcMessageBufferManager { + private readonly SystemTextJsonFormatter formatter; + private Exception? resultDeserializationException; + internal JsonRpcResult(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + internal JsonElement? JsonResult { get; set; } public void DeserializationComplete(JsonRpcMessage message) @@ -357,7 +354,7 @@ public override T GetResult() return this.JsonResult is null ? (T)this.Result! - : this.JsonResult.Value.Deserialize()!; + : this.JsonResult.Value.Deserialize(this.formatter.JsonSerializerOptions)!; } protected internal override void SetExpectedResultType(Type resultType) @@ -366,7 +363,7 @@ protected internal override void SetExpectedResultType(Type resultType) try { - this.Result = this.JsonResult.Value.Deserialize(resultType); + this.Result = this.JsonResult.Value.Deserialize(resultType, this.formatter.JsonSerializerOptions); this.JsonResult = default; } catch (Exception ex) diff --git a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt index 51f671c8..5fbd4f9e 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt @@ -4,5 +4,7 @@ StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequenc StreamJsonRpc.SystemTextJsonFormatter.Encoding.get -> System.Text.Encoding! StreamJsonRpc.SystemTextJsonFormatter.Encoding.set -> void StreamJsonRpc.SystemTextJsonFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> object! +StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.get -> System.Text.Json.JsonSerializerOptions! +StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.set -> void StreamJsonRpc.SystemTextJsonFormatter.Serialize(System.Buffers.IBufferWriter! bufferWriter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void \ No newline at end of file diff --git a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt index 51f671c8..5fbd4f9e 100644 --- a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt @@ -4,5 +4,7 @@ StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequenc StreamJsonRpc.SystemTextJsonFormatter.Encoding.get -> System.Text.Encoding! StreamJsonRpc.SystemTextJsonFormatter.Encoding.set -> void StreamJsonRpc.SystemTextJsonFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> object! +StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.get -> System.Text.Json.JsonSerializerOptions! +StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.set -> void StreamJsonRpc.SystemTextJsonFormatter.Serialize(System.Buffers.IBufferWriter! bufferWriter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void \ No newline at end of file From 6a065a98f24c18112f81dc0c19e60b743c41aeab Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 18 Apr 2023 12:18:03 -0600 Subject: [PATCH 07/35] Support deserializing error data --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 76 +++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 81b2827f..c63654e7 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -80,10 +80,11 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding JsonRpcError error = new() { RequestId = ReadRequestId(), - Error = new JsonRpcError.ErrorDetail + Error = new JsonRpcError.ErrorDetail(this) { Code = (JsonRpcErrorCode)errorElement.GetProperty(Utf8Strings.code).GetInt64(), Message = errorElement.GetProperty(Utf8Strings.message).GetString(), + JsonData = errorElement.TryGetProperty(Utf8Strings.data, out JsonElement dataElement) ? dataElement : null, }, }; @@ -150,7 +151,7 @@ public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message) WriteResult(result); writer.WriteEndObject(); break; - case JsonRpcError error: + case Protocol.JsonRpcError error: WriteId(error.RequestId); WriteError(error); writer.WriteEndObject(); @@ -222,7 +223,7 @@ void WriteResult(Protocol.JsonRpcResult result) WriteUserData(result.Result); } - void WriteError(JsonRpcError error) + void WriteError(Protocol.JsonRpcError error) { if (error.Error is null) { @@ -373,4 +374,73 @@ protected internal override void SetExpectedResultType(Type resultType) } } } + + private class JsonRpcError : Protocol.JsonRpcError, IJsonRpcMessageBufferManager + { + internal new ErrorDetail? Error + { + get => (ErrorDetail?)base.Error; + set => base.Error = value; + } + + public void DeserializationComplete(JsonRpcMessage message) + { + Assumes.True(message == this); + + // Clear references to buffers that we are no longer entitled to. + if (this.Error is { } detail) + { + detail.JsonData = null; + } + } + + internal new class ErrorDetail : Protocol.JsonRpcError.ErrorDetail + { + private readonly SystemTextJsonFormatter formatter; + + internal ErrorDetail(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + + internal JsonElement? JsonData { get; set; } + + public override object? GetData(Type dataType) + { + Requires.NotNull(dataType, nameof(dataType)); + if (this.JsonData is null) + { + return this.Data; + } + + try + { + return this.JsonData.Value.Deserialize(dataType, this.formatter.JsonSerializerOptions); + } + catch (JsonException) + { + // Deserialization failed. Try returning array/dictionary based primitive objects. + try + { + return this.JsonData.Value.Deserialize(this.formatter.JsonSerializerOptions); + } + catch (JsonException) + { + return null; + } + } + } + + protected internal override void SetExpectedDataType(Type dataType) + { + Verify.Operation(this.JsonData is not null, "Data is no longer available or has already been deserialized."); + + this.Data = this.GetData(dataType); + + // Clear the source now that we've deserialized to prevent GetData from attempting + // deserialization later when the buffer may be recycled on another thread. + this.JsonData = default; + } + } + } } From 4a4d726d290cabbb45e26aa9f1f3197690e2b59a Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 18 Apr 2023 15:04:15 -0600 Subject: [PATCH 08/35] More tests passing --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 170 ++++++++++++++---- .../netstandard2.0/PublicAPI.Unshipped.txt | 1 - .../netstandard2.1/PublicAPI.Unshipped.txt | 1 - .../JsonRpcJsonHeadersTests.cs | 26 +++ test/StreamJsonRpc.Tests/JsonRpcTests.cs | 26 --- 5 files changed, 157 insertions(+), 67 deletions(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index c63654e7..1150df1a 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -5,6 +5,7 @@ using System.Runtime.ExceptionServices; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using StreamJsonRpc.Protocol; using StreamJsonRpc.Reflection; @@ -17,15 +18,34 @@ public partial class SystemTextJsonFormatter : IJsonRpcMessageFormatter, IJsonRp { private static readonly JsonWriterOptions WriterOptions = new() { }; - private static readonly JsonReaderOptions ReaderOptions = new() { }; - private static readonly JsonDocumentOptions DocumentOptions = new() { }; + /// + /// The to use for the envelope and built-in types. + /// + private static readonly JsonSerializerOptions BuiltInSerializerOptions = new() + { + Converters = + { + RequestIdJsonConverter.Instance, + }, + }; + /// /// UTF-8 encoding without a preamble. /// private static readonly Encoding DefaultEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + private JsonSerializerOptions massagedUserDataSerializerOptions; + + /// + /// Initializes a new instance of the class. + /// + public SystemTextJsonFormatter() + { + this.massagedUserDataSerializerOptions = this.MassageUserDataSerializerOptions(new()); + } + /// public Encoding Encoding { @@ -34,9 +54,12 @@ public Encoding Encoding } /// - /// Gets or sets the options to use when serializing and deserializing JSON containing user data. + /// Sets the options to use when serializing and deserializing JSON containing user data. /// - public JsonSerializerOptions JsonSerializerOptions { get; set; } = new(); + public JsonSerializerOptions JsonSerializerOptions + { + set => this.massagedUserDataSerializerOptions = this.MassageUserDataSerializerOptions(new(value)); + } /// public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer) => this.Deserialize(contentBuffer, this.Encoding); @@ -107,20 +130,9 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding RequestId ReadRequestId() { - if (document.RootElement.TryGetProperty(Utf8Strings.id, out JsonElement idElement)) - { - return idElement.ValueKind switch - { - JsonValueKind.Number => new RequestId(idElement.GetInt64()), - JsonValueKind.String => new RequestId(idElement.GetString()), - JsonValueKind.Null => new RequestId(null), - _ => throw new JsonException("Unexpected value kind for id property: " + idElement.ValueKind), - }; - } - else - { - return RequestId.NotSpecified; - } + return document.RootElement.TryGetProperty(Utf8Strings.id, out JsonElement idElement) + ? idElement.Deserialize(BuiltInSerializerOptions) + : RequestId.NotSpecified; } return returnValue; @@ -176,19 +188,12 @@ void WriteVersion() } } - void WriteId(RequestId requestId) + void WriteId(RequestId id) { - if (requestId.Number is long idNumber) + if (!id.IsEmpty) { - writer.WriteNumber(Utf8Strings.id, idNumber); - } - else if (requestId.String is string idString) - { - writer.WriteString(Utf8Strings.id, idString); - } - else - { - writer.WriteNull(Utf8Strings.id); + writer.WritePropertyName(Utf8Strings.id); + RequestIdJsonConverter.Instance.Write(writer, id, BuiltInSerializerOptions); } } @@ -244,10 +249,16 @@ void WriteError(Protocol.JsonRpcError error) void WriteUserData(object? value) { - JsonSerializer.Serialize(writer, value, this.JsonSerializerOptions); + JsonSerializer.Serialize(writer, value, this.massagedUserDataSerializerOptions); } } + private JsonSerializerOptions MassageUserDataSerializerOptions(JsonSerializerOptions options) + { + options.Converters.Add(RequestIdJsonConverter.Instance); + return options; + } + private static class Utf8Strings { #pragma warning disable SA1300 // Element should begin with upper-case letter @@ -277,19 +288,36 @@ private class JsonRpcRequest : Protocol.JsonRpcRequest, IJsonRpcMessageBufferMan { private readonly SystemTextJsonFormatter formatter; + private int argumentCount; + + private JsonElement? jsonArguments; + internal JsonRpcRequest(SystemTextJsonFormatter formatter) { this.formatter = formatter; } - internal JsonElement? JsonArguments { get; set; } + public override int ArgumentCount => this.argumentCount; + + internal JsonElement? JsonArguments + { + get => this.jsonArguments; + init + { + this.jsonArguments = value; + if (value.HasValue) + { + this.argumentCount = CountArguments(value.Value); + } + } + } public void DeserializationComplete(JsonRpcMessage message) { Assumes.True(message == this); // Clear references to buffers that we are no longer entitled to. - this.JsonArguments = null; + this.jsonArguments = null; } public override bool TryGetArgumentByNameOrIndex(string? name, int position, Type? typeHint, out object? value) @@ -320,9 +348,43 @@ public override bool TryGetArgumentByNameOrIndex(string? name, int position, Typ throw new JsonException("Unexpected value kind for arguments: " + this.JsonArguments?.ValueKind ?? "null"); } - value = valueElement?.Deserialize(typeHint ?? typeof(object), this.formatter.JsonSerializerOptions); + try + { + value = valueElement?.Deserialize(typeHint ?? typeof(object), this.formatter.massagedUserDataSerializerOptions); + } + catch (JsonException ex) + { + throw new RpcArgumentDeserializationException(name, position, typeHint, ex); + } + return valueElement.HasValue; } + + private static int CountArguments(JsonElement arguments) + { + int count = 0; + switch (arguments.ValueKind) + { + case JsonValueKind.Array: + foreach (JsonElement element in arguments.EnumerateArray()) + { + count++; + } + + break; + case JsonValueKind.Object: + foreach (JsonProperty property in arguments.EnumerateObject()) + { + count++; + } + + break; + default: + throw new InvalidOperationException("Unexpected value kind: " + arguments.ValueKind); + } + + return count; + } } private class JsonRpcResult : Protocol.JsonRpcResult, IJsonRpcMessageBufferManager @@ -355,7 +417,7 @@ public override T GetResult() return this.JsonResult is null ? (T)this.Result! - : this.JsonResult.Value.Deserialize(this.formatter.JsonSerializerOptions)!; + : this.JsonResult.Value.Deserialize(this.formatter.massagedUserDataSerializerOptions)!; } protected internal override void SetExpectedResultType(Type resultType) @@ -364,7 +426,7 @@ protected internal override void SetExpectedResultType(Type resultType) try { - this.Result = this.JsonResult.Value.Deserialize(resultType, this.formatter.JsonSerializerOptions); + this.Result = this.JsonResult.Value.Deserialize(resultType, this.formatter.massagedUserDataSerializerOptions); this.JsonResult = default; } catch (Exception ex) @@ -415,14 +477,14 @@ internal ErrorDetail(SystemTextJsonFormatter formatter) try { - return this.JsonData.Value.Deserialize(dataType, this.formatter.JsonSerializerOptions); + return this.JsonData.Value.Deserialize(dataType, this.formatter.massagedUserDataSerializerOptions); } catch (JsonException) { // Deserialization failed. Try returning array/dictionary based primitive objects. try { - return this.JsonData.Value.Deserialize(this.formatter.JsonSerializerOptions); + return this.JsonData.Value.Deserialize(this.formatter.massagedUserDataSerializerOptions); } catch (JsonException) { @@ -433,8 +495,6 @@ internal ErrorDetail(SystemTextJsonFormatter formatter) protected internal override void SetExpectedDataType(Type dataType) { - Verify.Operation(this.JsonData is not null, "Data is no longer available or has already been deserialized."); - this.Data = this.GetData(dataType); // Clear the source now that we've deserialized to prevent GetData from attempting @@ -443,4 +503,36 @@ protected internal override void SetExpectedDataType(Type dataType) } } } + + private class RequestIdJsonConverter : JsonConverter + { + internal static readonly RequestIdJsonConverter Instance = new(); + + public override RequestId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Number => new RequestId(reader.GetInt64()), + JsonTokenType.String => new RequestId(reader.GetString()), + JsonTokenType.Null => RequestId.Null, + _ => throw new JsonException("Unexpected token type for id property: " + reader.TokenType), + }; + } + + public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options) + { + if (value.Number is long idNumber) + { + writer.WriteNumberValue(idNumber); + } + else if (value.String is string idString) + { + writer.WriteStringValue(idString); + } + else + { + writer.WriteNullValue(); + } + } + } } diff --git a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt index 5fbd4f9e..d25eb0e0 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt @@ -4,7 +4,6 @@ StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequenc StreamJsonRpc.SystemTextJsonFormatter.Encoding.get -> System.Text.Encoding! StreamJsonRpc.SystemTextJsonFormatter.Encoding.set -> void StreamJsonRpc.SystemTextJsonFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> object! -StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.get -> System.Text.Json.JsonSerializerOptions! StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.set -> void StreamJsonRpc.SystemTextJsonFormatter.Serialize(System.Buffers.IBufferWriter! bufferWriter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void \ No newline at end of file diff --git a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt index 5fbd4f9e..d25eb0e0 100644 --- a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt @@ -4,7 +4,6 @@ StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequenc StreamJsonRpc.SystemTextJsonFormatter.Encoding.get -> System.Text.Encoding! StreamJsonRpc.SystemTextJsonFormatter.Encoding.set -> void StreamJsonRpc.SystemTextJsonFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> object! -StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.get -> System.Text.Json.JsonSerializerOptions! StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.set -> void StreamJsonRpc.SystemTextJsonFormatter.Serialize(System.Buffers.IBufferWriter! bufferWriter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void \ No newline at end of file diff --git a/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs b/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs index fb43e842..ee901682 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs @@ -44,6 +44,32 @@ public async Task CustomJsonConvertersAreNotAppliedToBaseMessage() Assert.Equal("YSE=", result); // a! } + [Fact] + public async Task CanInvokeServerMethodWithParameterPassedAsObject() + { + string result1 = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.TestParameter), new { test = "test" }); + Assert.Equal("object {" + Environment.NewLine + " \"test\": \"test\"" + Environment.NewLine + "}", result1); + } + + [Fact] + public async Task CanInvokeServerMethodWithParameterPassedAsArray() + { + string result1 = await this.clientRpc.InvokeAsync(nameof(Server.TestParameter), "test"); + Assert.Equal("object test", result1); + } + + [Fact] + public async Task InvokeWithCancellationAsync_AndCancel() + { + using (var cts = new CancellationTokenSource()) + { + var invokeTask = this.clientRpc.InvokeWithCancellationAsync(nameof(Server.AsyncMethodWithJTokenAndCancellation), new[] { "a" }, cts.Token); + await Task.WhenAny(invokeTask, this.server.ServerMethodReached.WaitAsync(this.TimeoutToken)); + cts.Cancel(); + await Assert.ThrowsAnyAsync(() => invokeTask); + } + } + [Fact] public async Task InvokeWithParameterObjectAsync_AndCancel() { diff --git a/test/StreamJsonRpc.Tests/JsonRpcTests.cs b/test/StreamJsonRpc.Tests/JsonRpcTests.cs index 373288a0..2591c347 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -2907,13 +2907,6 @@ public void JoinableTaskFactory_IntegrationClientSideOnly() }); } - [Fact] - public async Task CanInvokeServerMethodWithParameterPassedAsObject() - { - string result1 = await this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.TestParameter), new { test = "test" }); - Assert.Equal("object {" + Environment.NewLine + " \"test\": \"test\"" + Environment.NewLine + "}", result1); - } - [Fact] public async Task InvokeWithParameterObject_WithRenamingAttributes() { @@ -2922,25 +2915,6 @@ public async Task InvokeWithParameterObject_WithRenamingAttributes() Assert.Equal(param.TheArgument + "!", result); } - [Fact] - public async Task CanInvokeServerMethodWithParameterPassedAsArray() - { - string result1 = await this.clientRpc.InvokeAsync(nameof(Server.TestParameter), "test"); - Assert.Equal("object test", result1); - } - - [Fact] - public async Task InvokeWithCancellationAsync_AndCancel() - { - using (var cts = new CancellationTokenSource()) - { - var invokeTask = this.clientRpc.InvokeWithCancellationAsync(nameof(Server.AsyncMethodWithJTokenAndCancellation), new[] { "a" }, cts.Token); - await Task.WhenAny(invokeTask, this.server.ServerMethodReached.WaitAsync(this.TimeoutToken)); - cts.Cancel(); - await Assert.ThrowsAnyAsync(() => invokeTask); - } - } - [Fact] public virtual async Task CanPassAndCallPrivateMethodsObjects() { From db38a44ad6936b1f36a1f9f577fbce91ae88f64b Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 18 Apr 2023 16:49:37 -0600 Subject: [PATCH 09/35] Add support for DataContractSerializer attributes --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 130 ++++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 1150df1a..9e83cbea 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -2,10 +2,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Runtime.ExceptionServices; +using System.Runtime.Serialization; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using StreamJsonRpc.Protocol; using StreamJsonRpc.Reflection; @@ -43,7 +47,14 @@ public partial class SystemTextJsonFormatter : IJsonRpcMessageFormatter, IJsonRp /// public SystemTextJsonFormatter() { - this.massagedUserDataSerializerOptions = this.MassageUserDataSerializerOptions(new()); + this.massagedUserDataSerializerOptions = this.MassageUserDataSerializerOptions(new() + { + // Fields are important because anonymous types are emitted with fields, not properties. + IncludeFields = true, + + // Provide compatibility with DataContractSerializer attributes by default. + TypeInfoResolver = DataContractResolver.OnlyDecoratedTypes, + }); } /// @@ -220,6 +231,12 @@ void WriteArguments(Protocol.JsonRpcRequest request) writer.WriteEndObject(); } + else if (request.Arguments is not null) + { + // This is a custom named arguments object, so we'll just serialize it as-is. + writer.WritePropertyName(Utf8Strings.@params); + WriteUserData(request.Arguments); + } } void WriteResult(Protocol.JsonRpcResult result) @@ -322,6 +339,12 @@ public void DeserializationComplete(JsonRpcMessage message) public override bool TryGetArgumentByNameOrIndex(string? name, int position, Type? typeHint, out object? value) { + if (this.JsonArguments is null) + { + value = null; + return false; + } + JsonElement? valueElement = null; switch (this.JsonArguments?.ValueKind) { @@ -345,7 +368,7 @@ public override bool TryGetArgumentByNameOrIndex(string? name, int position, Typ break; default: - throw new JsonException("Unexpected value kind for arguments: " + this.JsonArguments?.ValueKind ?? "null"); + throw new JsonException("Unexpected value kind for arguments: " + (this.JsonArguments?.ValueKind.ToString() ?? "null")); } try @@ -535,4 +558,107 @@ public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerialize } } } + + private class DataContractResolver : IJsonTypeInfoResolver + { + internal static readonly DataContractResolver OnlyDecoratedTypes = new(onlyRecognizeDecoratedTypes: true); + + private readonly Dictionary typeInfoCache = new(); + + private readonly bool onlyRecognizeDecoratedTypes; + + private readonly DefaultJsonTypeInfoResolver fallbackResolver = new(); + + private DataContractResolver(bool onlyRecognizeDecoratedTypes) + { + this.onlyRecognizeDecoratedTypes = onlyRecognizeDecoratedTypes; + } + + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + if (!this.typeInfoCache.TryGetValue(type, out JsonTypeInfo? typeInfo)) + { + DataContractAttribute? dataContractAttribute = type.GetCustomAttribute(); + if (dataContractAttribute is not null || !this.onlyRecognizeDecoratedTypes) + { + typeInfo = JsonTypeInfo.CreateJsonTypeInfo(type, options); + + typeInfo.CreateObject = () => FormatterServices.GetUninitializedObject(type); + PopulateMembersInfos(type, typeInfo, dataContractAttribute); + } + else + { + typeInfo = this.fallbackResolver.GetTypeInfo(type, options); + } + + this.typeInfoCache.Add(type, typeInfo); + } + + return typeInfo; + } + + private static void PopulateMembersInfos(Type type, JsonTypeInfo jsonTypeInfo, DataContractAttribute? dataContractAttribute) + { + BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance; + + // When the type is decorated with DataContractAttribute, we can consider non-public members. + if (dataContractAttribute is not null) + { + bindingFlags |= BindingFlags.NonPublic; + } + + foreach (PropertyInfo propertyInfo in type.GetProperties(bindingFlags)) + { + if (TryCreateJsonPropertyInfo(propertyInfo, propertyInfo.PropertyType, out JsonPropertyInfo? jsonPropertyInfo)) + { + if (propertyInfo.CanRead) + { + jsonPropertyInfo.Get = propertyInfo.GetValue; + } + + if (propertyInfo.CanWrite) + { + jsonPropertyInfo.Set = propertyInfo.SetValue; + } + } + } + + foreach (FieldInfo fieldInfo in type.GetFields(bindingFlags)) + { + if (TryCreateJsonPropertyInfo(fieldInfo, fieldInfo.FieldType, out JsonPropertyInfo? jsonPropertyInfo)) + { + jsonPropertyInfo.Get = fieldInfo.GetValue; + if (!fieldInfo.IsInitOnly) + { + jsonPropertyInfo.Set = fieldInfo.SetValue; + } + } + } + + bool TryCreateJsonPropertyInfo(MemberInfo memberInfo, Type propertyType, [NotNullWhen(true)] out JsonPropertyInfo? jsonPropertyInfo) + { + DataMemberAttribute? dataMemberAttribute = memberInfo.GetCustomAttribute(); + if (dataContractAttribute is null || dataMemberAttribute is not null) + { + jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(propertyType, dataMemberAttribute?.Name ?? memberInfo.Name); + if (dataMemberAttribute is not null) + { + jsonPropertyInfo.Order = dataMemberAttribute.Order; + jsonPropertyInfo.IsRequired = dataMemberAttribute.IsRequired; + if (!dataMemberAttribute.EmitDefaultValue) + { + object? defaultValue = propertyType.IsValueType ? FormatterServices.GetUninitializedObject(propertyType) : null; + jsonPropertyInfo.ShouldSerialize = (_, value) => !object.Equals(defaultValue, value); + } + } + + jsonTypeInfo.Properties.Add(jsonPropertyInfo); + return true; + } + + jsonPropertyInfo = null; + return false; + } + } + } } From ed068f62140b5ea92ed7e88447474d29e698b7df Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 19 Apr 2023 16:01:18 -0600 Subject: [PATCH 10/35] Consolidate shared formatter behavior in a new base class --- src/StreamJsonRpc/FormatterBase.cs | 246 ++++++++++++++++++ src/StreamJsonRpc/JsonMessageFormatter.cs | 233 +++-------------- src/StreamJsonRpc/MessagePackFormatter.cs | 234 +++-------------- .../netstandard2.0/PublicAPI.Shipped.txt | 5 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 25 +- .../netstandard2.1/PublicAPI.Shipped.txt | 5 +- .../netstandard2.1/PublicAPI.Unshipped.txt | 25 +- 7 files changed, 364 insertions(+), 409 deletions(-) create mode 100644 src/StreamJsonRpc/FormatterBase.cs diff --git a/src/StreamJsonRpc/FormatterBase.cs b/src/StreamJsonRpc/FormatterBase.cs new file mode 100644 index 00000000..e0fc4271 --- /dev/null +++ b/src/StreamJsonRpc/FormatterBase.cs @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO.Pipelines; +using Nerdbank.Streams; +using StreamJsonRpc.Protocol; +using StreamJsonRpc.Reflection; + +namespace StreamJsonRpc; + +/// +/// A base class for implementations +/// that support exotic types. +/// +public abstract class FormatterBase : IJsonRpcFormatterState, IJsonRpcInstanceContainer, IDisposable +{ + private JsonRpc? rpc; + + /// + /// Backing field for the property. + /// + private MultiplexingStream? multiplexingStream; + + /// + /// The we use to support method arguments. + /// + private MessageFormatterProgressTracker? formatterProgressTracker; + + /// + /// The helper for marshaling pipes as RPC method arguments. + /// + private MessageFormatterDuplexPipeTracker? duplexPipeTracker; + + /// + /// The tracker we use to support transmission of types. + /// + private MessageFormatterEnumerableTracker? enumerableTracker; + + /// + /// The helper for marshaling in RPC method arguments or return values. + /// + private MessageFormatterRpcMarshaledContextTracker? rpcMarshaledContextTracker; + + /// + /// Initializes a new instance of the class. + /// + public FormatterBase() + { + } + + /// + public RequestId SerializingMessageWithId { get; private set; } + + /// + public RequestId DeserializingMessageWithId { get; private set; } + + /// + public bool SerializingRequest { get; private set; } + + /// + JsonRpc IJsonRpcInstanceContainer.Rpc + { + set + { + Verify.Operation(this.rpc is null, Resources.FormatterConfigurationLockedAfterJsonRpcAssigned); + if (value is not null) + { + this.rpc = value; + + this.formatterProgressTracker = new MessageFormatterProgressTracker(value, this); + this.enumerableTracker = new MessageFormatterEnumerableTracker(value, this); + this.duplexPipeTracker = new MessageFormatterDuplexPipeTracker(value, this) { MultiplexingStream = this.MultiplexingStream }; + this.rpcMarshaledContextTracker = new MessageFormatterRpcMarshaledContextTracker(value, this); + } + } + } + + /// + /// Gets or sets the that may be used to establish out of band communication (e.g. marshal arguments). + /// + public MultiplexingStream? MultiplexingStream + { + get => this.multiplexingStream; + set + { + Verify.Operation(this.JsonRpc is null, Resources.FormatterConfigurationLockedAfterJsonRpcAssigned); + this.multiplexingStream = value; + } + } + + /// + /// Gets the that is associated with this formatter. + /// + /// + /// This field is used to create the instance that will send the progress notifications when server reports it. + /// The property helps to ensure that only one instance is associated with this formatter. + /// + protected JsonRpc? JsonRpc => this.rpc; + + /// + /// Gets the instance containing useful methods to help on the implementation of message formatters. + /// + protected MessageFormatterProgressTracker FormatterProgressTracker + { + get + { + Assumes.NotNull(this.formatterProgressTracker); // This should have been set in the Rpc property setter. + return this.formatterProgressTracker; + } + } + + /// + /// Gets the helper for marshaling pipes as RPC method arguments. + /// + protected MessageFormatterDuplexPipeTracker DuplexPipeTracker + { + get + { + Assumes.NotNull(this.duplexPipeTracker); // This should have been set in the Rpc property setter. + return this.duplexPipeTracker; + } + } + + /// + /// Gets the helper for marshaling in RPC method arguments or return values. + /// + protected MessageFormatterEnumerableTracker EnumerableTracker + { + get + { + Assumes.NotNull(this.enumerableTracker); // This should have been set in the Rpc property setter. + return this.enumerableTracker; + } + } + + /// + /// Gets the helper for marshaling in RPC method arguments or return values. + /// + private protected MessageFormatterRpcMarshaledContextTracker RpcMarshaledContextTracker + { + get + { + Assumes.NotNull(this.rpcMarshaledContextTracker); // This should have been set in the Rpc property setter. + return this.rpcMarshaledContextTracker; + } + } + + /// + /// Gets the message whose arguments are being deserialized. + /// + private protected JsonRpcMessage? DeserializingMessage { get; private set; } + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes managed and native resources held by this instance. + /// + /// if being disposed; if being finalized. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this.duplexPipeTracker?.Dispose(); + } + } + + /// + /// Sets up state to track deserialization of a message. + /// + /// The object being deserialized. + /// A value to dispose of when deserialization has completed. + protected DeserializationTracking TrackDeserialization(JsonRpcMessage message) => new(this, message); + + /// + /// Sets up state to track serialization of a message. + /// + /// The message being serialized. + /// A value to dispose of when serialization has completed. + protected SerializationTracking TrackSerialization(JsonRpcMessage message) => new(this, message); + + /// + /// Tracks deserialization of a message. + /// + public struct DeserializationTracking : IDisposable + { + private readonly FormatterBase formatter; + + /// + /// Initializes a new instance of the struct. + /// + /// The formatter. + /// The message being deserialized. + public DeserializationTracking(FormatterBase formatter, JsonRpcMessage message) + { + // Deserialization of messages should never occur concurrently for a single instance of a formatter. + Assumes.True(formatter.DeserializingMessageWithId.IsEmpty); + + this.formatter = formatter; + this.formatter.DeserializingMessage = message; + this.formatter.DeserializingMessageWithId = (message as IJsonRpcMessageWithId)?.RequestId ?? default; + } + + /// + /// Clears deserialization state. + /// + public void Dispose() + { + this.formatter.DeserializingMessageWithId = default; + this.formatter.DeserializingMessage = null; + } + } + + /// + /// Tracks serialization of a message. + /// + public struct SerializationTracking : IDisposable + { + private readonly FormatterBase formatter; + + /// + /// Initializes a new instance of the struct. + /// + /// The formatter. + /// The message being serialized. + public SerializationTracking(FormatterBase formatter, JsonRpcMessage message) + { + this.formatter = formatter; + this.formatter.SerializingMessageWithId = (message as IJsonRpcMessageWithId)?.RequestId ?? default; + this.formatter.SerializingRequest = message is JsonRpcRequest; + } + + /// + /// Clears serialization state. + /// + public void Dispose() + { + this.formatter.SerializingMessageWithId = default; + this.formatter.SerializingRequest = false; + } + } +} diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index e7eabb20..69d85095 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -26,7 +26,7 @@ namespace StreamJsonRpc; /// /// Each instance of this class may only be used with a single instance. /// -public class JsonMessageFormatter : IJsonRpcAsyncMessageTextFormatter, IJsonRpcFormatterState, IJsonRpcInstanceContainer, IJsonRpcMessageFactory, IDisposable +public class JsonMessageFormatter : FormatterBase, IJsonRpcAsyncMessageTextFormatter, IJsonRpcMessageFactory { /// /// The key into an dictionary whose value may be a that failed deserialization. @@ -82,46 +82,6 @@ public class JsonMessageFormatter : IJsonRpcAsyncMessageTextFormatter, IJsonRpcF /// private readonly object syncObject = new(); - /// - /// Backing field for the property. - /// - private MultiplexingStream? multiplexingStream; - - /// - /// instance containing useful methods to help on the implementation of message formatters. - /// - private MessageFormatterProgressTracker? formatterProgressTracker; - - /// - /// The helper for marshaling pipes as RPC method arguments. - /// - private MessageFormatterDuplexPipeTracker? duplexPipeTracker; - - /// - /// The helper for marshaling in RPC method arguments or return values. - /// - private MessageFormatterEnumerableTracker? enumerableTracker; - - /// - /// The helper for marshaling in RPC method arguments or return values. - /// - private MessageFormatterRpcMarshaledContextTracker? rpcMarshaledContextTracker; - - /// - /// Backing field for the property. - /// - private RequestId serializingMessageWithId; - - /// - /// Backing field for the property. - /// - private RequestId deserializingMessageWithId; - - /// - /// Backing field for the property. - /// - private bool serializingRequest; - /// /// A value indicating whether a request where is a /// has been transmitted. @@ -146,20 +106,6 @@ public class JsonMessageFormatter : IJsonRpcAsyncMessageTextFormatter, IJsonRpcF [DebuggerBrowsable(DebuggerBrowsableState.Never)] private Encoding encoding; - /// - /// Backing field for the property. - /// - /// - /// This field is used to create the instance that will send the progress notifications when server reports it. - /// The property helps to ensure that only one instance is associated with this formatter. - /// - private JsonRpc? rpc; - - /// - /// The message whose arguments are being deserialized. - /// - private JsonRpcMessage? deserializingMessage; - /// /// Whether has been executed. /// @@ -248,90 +194,11 @@ public Version ProtocolVersion /// public JsonSerializer JsonSerializer { get; } - /// - /// Gets or sets the that may be used to establish out of band communication (e.g. marshal arguments). - /// - public MultiplexingStream? MultiplexingStream - { - get => this.multiplexingStream; - set - { - Verify.Operation(this.rpc is null, Resources.FormatterConfigurationLockedAfterJsonRpcAssigned); - this.multiplexingStream = value; - } - } - - /// - JsonRpc IJsonRpcInstanceContainer.Rpc + /// + public new MultiplexingStream? MultiplexingStream { - set - { - Requires.NotNull(value, nameof(value)); - Verify.Operation(this.rpc is null, Resources.FormatterConfigurationLockedAfterJsonRpcAssigned); - this.rpc = value; - - this.formatterProgressTracker = new MessageFormatterProgressTracker(value, this); - this.enumerableTracker = new MessageFormatterEnumerableTracker(value, this); - this.duplexPipeTracker = new MessageFormatterDuplexPipeTracker(value, this) { MultiplexingStream = this.multiplexingStream }; - this.rpcMarshaledContextTracker = new MessageFormatterRpcMarshaledContextTracker(value, this); - } - } - - /// - RequestId IJsonRpcFormatterState.SerializingMessageWithId => this.serializingMessageWithId; - - /// - RequestId IJsonRpcFormatterState.DeserializingMessageWithId => this.deserializingMessageWithId; - - /// - bool IJsonRpcFormatterState.SerializingRequest => this.serializingRequest; - - /// - /// Gets the instance containing useful methods to help on the implementation of message formatters. - /// - private MessageFormatterProgressTracker FormatterProgressTracker - { - get - { - Assumes.NotNull(this.formatterProgressTracker); // This should have been set in the Rpc property setter. - return this.formatterProgressTracker; - } - } - - /// - /// Gets the helper for marshaling pipes as RPC method arguments. - /// - private MessageFormatterDuplexPipeTracker DuplexPipeTracker - { - get - { - Assumes.NotNull(this.duplexPipeTracker); // This should have been set in the Rpc property setter. - return this.duplexPipeTracker; - } - } - - /// - /// Gets the helper for marshaling in RPC method arguments or return values. - /// - private MessageFormatterEnumerableTracker EnumerableTracker - { - get - { - Assumes.NotNull(this.enumerableTracker); // This should have been set in the Rpc property setter. - return this.enumerableTracker; - } - } - - /// - /// Gets the helper for marshaling in RPC method arguments or return values. - /// - private MessageFormatterRpcMarshaledContextTracker RpcMarshaledContextTracker - { - get - { - Assumes.NotNull(this.rpcMarshaledContextTracker); // This should have been set in the Rpc property setter. - return this.rpcMarshaledContextTracker; - } + get => base.MultiplexingStream; + set => base.MultiplexingStream = value; } /// @@ -345,7 +212,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding JToken json = this.ReadJToken(contentBuffer, encoding); JsonRpcMessage message = this.Deserialize(json); - IJsonRpcTracingCallbacks? tracingCallbacks = this.rpc; + IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc; tracingCallbacks?.OnMessageDeserialized(message, json); return message; @@ -363,7 +230,7 @@ public async ValueTask DeserializeAsync(PipeReader reader, Encod JToken json = await JToken.ReadFromAsync(jsonReader, LoadSettings, cancellationToken).ConfigureAwait(false); JsonRpcMessage message = this.Deserialize(json); - IJsonRpcTracingCallbacks? tracingCallbacks = this.rpc; + IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc; tracingCallbacks?.OnMessageDeserialized(message, json); return message; @@ -378,7 +245,7 @@ public void Serialize(IBufferWriter contentBuffer, JsonRpcMessage message) { JToken json = this.Serialize(message); - IJsonRpcTracingCallbacks? tracingCallbacks = this.rpc; + IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc; tracingCallbacks?.OnMessageSerialized(message, json); this.WriteJToken(contentBuffer, json); @@ -497,22 +364,16 @@ public JToken Serialize(JsonRpcMessage message) /// Protocol.JsonRpcResult IJsonRpcMessageFactory.CreateResultMessage() => new JsonRpcResult(this.JsonSerializer); - /// - public void Dispose() + /// + protected override void Dispose(bool disposing) { - this.Dispose(true); - GC.SuppressFinalize(this); - } + if (disposing) + { + this.sequenceTextReader.Dispose(); + this.bufferTextWriter.Dispose(); + } - /// - /// Disposes managed and native resources held by this instance. - /// - /// if being disposed; if being finalized. - protected virtual void Dispose(bool disposing) - { - this.duplexPipeTracker?.Dispose(); - this.sequenceTextReader.Dispose(); - this.bufferTextWriter.Dispose(); + base.Dispose(disposing); } private static IReadOnlyDictionary PartiallyParseNamedArguments(JObject args) @@ -623,14 +484,11 @@ private void ConfigureJsonTextReader(JsonTextReader reader) /// A JSON-RPC message. private void TokenizeUserData(JsonRpcMessage jsonRpcMessage) { - try + using (this.TrackSerialization(jsonRpcMessage)) { - this.serializingMessageWithId = jsonRpcMessage is IJsonRpcMessageWithId msgWithId ? msgWithId.RequestId : default; switch (jsonRpcMessage) { case Protocol.JsonRpcRequest request: - this.serializingRequest = true; - if (request.ArgumentsList is not null) { var tokenizedArgumentsList = new JToken[request.ArgumentsList.Count]; @@ -671,11 +529,6 @@ private void TokenizeUserData(JsonRpcMessage jsonRpcMessage) break; } } - finally - { - this.serializingMessageWithId = default; - this.serializingRequest = false; - } } /// @@ -755,7 +608,7 @@ private JsonRpcRequest ReadRequest(JToken json) // If method is $/progress, get the progress instance from the dictionary and call Report string? method = json.Value("method"); - if (this.formatterProgressTracker is not null && string.Equals(method, MessageFormatterProgressTracker.ProgressRequestSpecialMethod, StringComparison.Ordinal)) + if (this.JsonRpc is not null && string.Equals(method, MessageFormatterProgressTracker.ProgressRequestSpecialMethod, StringComparison.Ordinal)) { try { @@ -770,7 +623,7 @@ private JsonRpcRequest ReadRequest(JToken json) null; MessageFormatterProgressTracker.ProgressParamInformation? progressInfo = null; - if (progressId is object && this.formatterProgressTracker.TryGetProgressObject(progressId.Value(), out progressInfo)) + if (progressId is object && this.FormatterProgressTracker.TryGetProgressObject(progressId.Value(), out progressInfo)) { object? typedValue = value?.ToObject(progressInfo.ValueType, this.JsonSerializer); progressInfo.InvokeReport(typedValue); @@ -780,7 +633,7 @@ private JsonRpcRequest ReadRequest(JToken json) catch (Exception e) #pragma warning restore CA1031 // Do not catch general exception types { - this.rpc?.TraceSource.TraceData(TraceEventType.Error, (int)JsonRpc.TraceEvents.ProgressNotificationError, e); + this.JsonRpc.TraceSource.TraceData(TraceEventType.Error, (int)JsonRpc.TraceEvents.ProgressNotificationError, e); } } @@ -951,7 +804,7 @@ internal JsonRpcRequest(JsonMessageFormatter formatter) public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan parameters, Span typedArguments) { // Consider the attribute applied to the particular overload that we're considering right now. - this.ApplicableMethodAttribute = this.Method is not null ? this.formatter.rpc?.GetJsonRpcMethodAttribute(this.Method, parameters) : null; + this.ApplicableMethodAttribute = this.Method is not null ? this.formatter.JsonRpc?.GetJsonRpcMethodAttribute(this.Method, parameters) : null; try { if (parameters.Length == 1 && this.NamedArguments is not null) @@ -980,19 +833,10 @@ public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan(); - return this.jsonMessageFormatter.duplexPipeTracker!.GetPipe(tokenId); + return this.jsonMessageFormatter.DuplexPipeTracker!.GetPipe(tokenId); } public override void WriteJson(JsonWriter writer, IDuplexPipe? value, JsonSerializer serializer) { - ulong? token = this.jsonMessageFormatter.duplexPipeTracker!.GetULongToken(value); + ulong? token = this.jsonMessageFormatter.DuplexPipeTracker!.GetULongToken(value); writer.WriteValue(token); } } @@ -1594,7 +1429,7 @@ internal ExceptionConverter(JsonMessageFormatter formatter) public override Exception? ReadJson(JsonReader reader, Type objectType, Exception? existingValue, bool hasExistingValue, JsonSerializer serializer) { - Assumes.NotNull(this.formatter.rpc); + Assumes.NotNull(this.formatter.JsonRpc); if (reader.TokenType == JsonToken.Null) { return null; @@ -1608,7 +1443,7 @@ internal ExceptionConverter(JsonMessageFormatter formatter) throw new InvalidOperationException("Expected a StartObject token."); } - if (exceptionRecursionCounter.Value > this.formatter.rpc.ExceptionOptions.RecursionLimit) + if (exceptionRecursionCounter.Value > this.formatter.JsonRpc.ExceptionOptions.RecursionLimit) { // Exception recursion has gone too deep. Skip this value and return null as if there were no inner exception. // Note that in skipping, the parser may use recursion internally and may still throw if its own limits are exceeded. @@ -1641,7 +1476,7 @@ internal ExceptionConverter(JsonMessageFormatter formatter) } } - return ExceptionSerializationHelpers.Deserialize(this.formatter.rpc, info, this.formatter.rpc?.TraceSource); + return ExceptionSerializationHelpers.Deserialize(this.formatter.JsonRpc, info, this.formatter.JsonRpc?.TraceSource); } finally { @@ -1662,7 +1497,7 @@ public override void WriteJson(JsonWriter writer, Exception? value, JsonSerializ exceptionRecursionCounter.Value++; try { - if (exceptionRecursionCounter.Value > this.formatter.rpc?.ExceptionOptions.RecursionLimit) + if (exceptionRecursionCounter.Value > this.formatter.JsonRpc?.ExceptionOptions.RecursionLimit) { // Exception recursion has gone too deep. Skip this value and write null as if there were no inner exception. writer.WriteNull(); diff --git a/src/StreamJsonRpc/MessagePackFormatter.cs b/src/StreamJsonRpc/MessagePackFormatter.cs index 8dd0090b..8668f08b 100644 --- a/src/StreamJsonRpc/MessagePackFormatter.cs +++ b/src/StreamJsonRpc/MessagePackFormatter.cs @@ -27,7 +27,7 @@ namespace StreamJsonRpc; /// The README on that project site describes use cases and its performance compared to alternative /// .NET MessagePack implementations and this one appears to be the best by far. /// -public class MessagePackFormatter : IJsonRpcMessageFormatter, IJsonRpcInstanceContainer, IJsonRpcFormatterState, IJsonRpcFormatterTracingCallbacks, IJsonRpcMessageFactory, IDisposable +public class MessagePackFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcFormatterTracingCallbacks, IJsonRpcMessageFactory { /// /// The constant "jsonrpc", in its various forms. @@ -111,61 +111,11 @@ public class MessagePackFormatter : IJsonRpcMessageFormatter, IJsonRpcInstanceCo private readonly ToStringHelper deserializationToStringHelper = new ToStringHelper(); - /// - /// Backing field for the property. - /// - private MultiplexingStream? multiplexingStream; - - /// - /// The we use to support method arguments. - /// - private MessageFormatterProgressTracker? formatterProgressTracker; - - /// - /// The helper for marshaling pipes as RPC method arguments. - /// - private MessageFormatterDuplexPipeTracker? duplexPipeTracker; - - /// - /// The tracker we use to support transmission of types. - /// - private MessageFormatterEnumerableTracker? enumerableTracker; - - /// - /// The helper for marshaling in RPC method arguments or return values. - /// - private MessageFormatterRpcMarshaledContextTracker? rpcMarshaledContextTracker; - - /// - /// Backing field for the property. - /// - private RequestId serializingMessageWithId; - - /// - /// Backing field for the property. - /// - private RequestId deserializingMessageWithId; - - /// - /// The message whose arguments are being deserialized. - /// - private JsonRpcMessage? deserializingMessage; - - /// - /// Backing field for the property. - /// - private bool serializingRequest; - /// /// The options to use for serializing user data (e.g. arguments, return values and errors). /// private MessagePackSerializerOptions userDataSerializationOptions; - /// - /// Backing field for the property. - /// - private JsonRpc? rpc; - /// /// Initializes a new instance of the class. /// @@ -213,93 +163,11 @@ private interface IMessageWithTopLevelPropertyBag public static MessagePackSerializerOptions DefaultUserDataSerializationOptions { get; } = StandardResolverAllowPrivate.Options .WithSecurity(MessagePackSecurity.UntrustedData); - /// - JsonRpc IJsonRpcInstanceContainer.Rpc + /// + public new MultiplexingStream? MultiplexingStream { - set - { - Verify.Operation(this.rpc is null, "This formatter already belongs to another JsonRpc instance. Create a new instance of this formatter for each new JsonRpc instance."); - - this.rpc = value; - - if (value is not null) - { - this.formatterProgressTracker = new MessageFormatterProgressTracker(value, this); - this.duplexPipeTracker = new MessageFormatterDuplexPipeTracker(value, this) { MultiplexingStream = this.multiplexingStream }; - this.enumerableTracker = new MessageFormatterEnumerableTracker(value, this); - this.rpcMarshaledContextTracker = new MessageFormatterRpcMarshaledContextTracker(value, this); - } - } - } - - /// - /// Gets or sets the that may be used to establish out of band communication (e.g. marshal arguments). - /// - public MultiplexingStream? MultiplexingStream - { - get => this.multiplexingStream; - set - { - Verify.Operation(this.rpc is null, Resources.FormatterConfigurationLockedAfterJsonRpcAssigned); - this.multiplexingStream = value; - } - } - - /// - RequestId IJsonRpcFormatterState.SerializingMessageWithId => this.serializingMessageWithId; - - /// - RequestId IJsonRpcFormatterState.DeserializingMessageWithId => this.deserializingMessageWithId; - - /// - bool IJsonRpcFormatterState.SerializingRequest => this.serializingRequest; - - /// - /// Gets the instance containing useful methods to help on the implementation of message formatters. - /// - private MessageFormatterProgressTracker FormatterProgressTracker - { - get - { - Assumes.NotNull(this.formatterProgressTracker); // This should have been set in the Rpc property setter. - return this.formatterProgressTracker; - } - } - - /// - /// Gets the helper for marshaling pipes as RPC method arguments. - /// - private MessageFormatterDuplexPipeTracker DuplexPipeTracker - { - get - { - Assumes.NotNull(this.duplexPipeTracker); // This should have been set in the Rpc property setter. - return this.duplexPipeTracker; - } - } - - /// - /// Gets the helper for marshaling in RPC method arguments or return values. - /// - private MessageFormatterEnumerableTracker EnumerableTracker - { - get - { - Assumes.NotNull(this.enumerableTracker); // This should have been set in the Rpc property setter. - return this.enumerableTracker; - } - } - - /// - /// Gets the helper for marshaling in RPC method arguments or return values. - /// - private MessageFormatterRpcMarshaledContextTracker RpcMarshaledContextTracker - { - get - { - Assumes.NotNull(this.rpcMarshaledContextTracker); // This should have been set in the Rpc property setter. - return this.rpcMarshaledContextTracker; - } + get => base.MultiplexingStream; + set => base.MultiplexingStream = value; } /// @@ -320,7 +188,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer) { JsonRpcMessage message = MessagePackSerializer.Deserialize(contentBuffer, this.messageSerializationOptions); - IJsonRpcTracingCallbacks? tracingCallbacks = this.rpc; + IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc; this.deserializationToStringHelper.Activate(contentBuffer, this.messageSerializationOptions); try { @@ -374,7 +242,7 @@ public void Serialize(IBufferWriter contentBuffer, JsonRpcMessage message) void IJsonRpcFormatterTracingCallbacks.OnSerializationComplete(JsonRpcMessage message, ReadOnlySequence encodedMessage) { - IJsonRpcTracingCallbacks? tracingCallbacks = this.rpc; + IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc; this.serializationToStringHelper.Activate(encodedMessage, this.messageSerializationOptions); try { @@ -386,22 +254,6 @@ void IJsonRpcFormatterTracingCallbacks.OnSerializationComplete(JsonRpcMessage me } } - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes managed and native resources held by this instance. - /// - /// if being disposed; if being finalized. - protected virtual void Dispose(bool disposing) - { - this.duplexPipeTracker?.Dispose(); - } - /// /// Extracts a dictionary of property names and values from the specified params object. /// @@ -1035,10 +887,10 @@ public TClass Deserialize(ref MessagePackReader reader, MessagePackSerializerOpt return default!; } - Assumes.NotNull(this.formatter.rpc); + Assumes.NotNull(this.formatter.JsonRpc); RawMessagePack token = RawMessagePack.ReadRaw(ref reader, copy: true); - bool clientRequiresNamedArgs = this.formatter.deserializingMessage is JsonRpcRequest { ApplicableMethodAttribute: { ClientRequiresNamedArguments: true } }; - return (TClass)this.formatter.FormatterProgressTracker.CreateProgress(this.formatter.rpc, token, typeof(TClass), clientRequiresNamedArgs); + bool clientRequiresNamedArgs = this.formatter.DeserializingMessage is JsonRpcRequest { ApplicableMethodAttribute: { ClientRequiresNamedArguments: true } }; + return (TClass)this.formatter.FormatterProgressTracker.CreateProgress(this.formatter.JsonRpc, token, typeof(TClass), clientRequiresNamedArgs); } public void Serialize(ref MessagePackWriter writer, TClass value, MessagePackSerializerOptions options) @@ -1548,7 +1400,7 @@ public ExceptionFormatter(MessagePackFormatter formatter) [return: MaybeNull] public T Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { - Assumes.NotNull(this.formatter.rpc); + Assumes.NotNull(this.formatter.JsonRpc); if (reader.TryReadNil()) { return null; @@ -1559,7 +1411,7 @@ public T Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions exceptionRecursionCounter.Value++; try { - if (exceptionRecursionCounter.Value > this.formatter.rpc.ExceptionOptions.RecursionLimit) + if (exceptionRecursionCounter.Value > this.formatter.JsonRpc.ExceptionOptions.RecursionLimit) { // Exception recursion has gone too deep. Skip this value and return null as if there were no inner exception. // Note that in skipping, the parser may use recursion internally and may still throw if its own limits are exceeded. @@ -1588,7 +1440,7 @@ public T Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions var resolverWrapper = options.Resolver as ResolverWrapper; Report.If(resolverWrapper is null, "Unexpected resolver type."); - return ExceptionSerializationHelpers.Deserialize(this.formatter.rpc, info, resolverWrapper?.Formatter.rpc?.TraceSource); + return ExceptionSerializationHelpers.Deserialize(this.formatter.JsonRpc, info, resolverWrapper?.Formatter.JsonRpc?.TraceSource); } finally { @@ -1607,7 +1459,7 @@ public void Serialize(ref MessagePackWriter writer, T? value, MessagePackSeriali exceptionRecursionCounter.Value++; try { - if (exceptionRecursionCounter.Value > this.formatter.rpc?.ExceptionOptions.RecursionLimit) + if (exceptionRecursionCounter.Value > this.formatter.JsonRpc?.ExceptionOptions.RecursionLimit) { // Exception recursion has gone too deep. Skip this value and write null as if there were no inner exception. writer.WriteNil(); @@ -1675,13 +1527,11 @@ public void Serialize(ref MessagePackWriter writer, JsonRpcMessage value, Messag { Requires.NotNull(value, nameof(value)); - this.formatter.serializingMessageWithId = value is IJsonRpcMessageWithId msgWithId ? msgWithId.RequestId : default; - try + using (this.formatter.TrackSerialization(value)) { switch (value) { case Protocol.JsonRpcRequest request: - this.formatter.serializingRequest = true; options.Resolver.GetFormatterWithVerify().Serialize(ref writer, request, options); break; case Protocol.JsonRpcResult result: @@ -1694,11 +1544,6 @@ public void Serialize(ref MessagePackWriter writer, JsonRpcMessage value, Messag throw new NotSupportedException("Unexpected JsonRpcMessage-derived type: " + value.GetType().Name); } } - finally - { - this.formatter.serializingMessageWithId = default; - this.formatter.serializingRequest = false; - } } } @@ -1820,7 +1665,7 @@ public Protocol.JsonRpcRequest Deserialize(ref MessagePackReader reader, Message catch (Exception ex) #pragma warning restore CA1031 // Do not catch general exception types { - this.formatter.rpc?.TraceSource.TraceData(TraceEventType.Error, (int)JsonRpc.TraceEvents.ProgressNotificationError, ex); + this.formatter.JsonRpc?.TraceSource.TraceData(TraceEventType.Error, (int)JsonRpc.TraceEvents.ProgressNotificationError, ex); } } @@ -2415,7 +2260,7 @@ void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan parameters, Span typedArguments) { // Consider the attribute applied to the particular overload that we're considering right now. - this.ApplicableMethodAttribute = this.Method is not null ? this.formatter.rpc?.GetJsonRpcMethodAttribute(this.Method, parameters) : null; + this.ApplicableMethodAttribute = this.Method is not null ? this.formatter.JsonRpc?.GetJsonRpcMethodAttribute(this.Method, parameters) : null; try { if (parameters.Length == 1 && this.MsgPackNamedArguments is not null) @@ -2427,22 +2272,17 @@ public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan void StreamJsonRpc.JsonMessageFormatter.DeserializeAsync(System.IO.Pipelines.PipeReader! reader, System.Text.Encoding! encoding, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask StreamJsonRpc.JsonMessageFormatter.DeserializeAsync(System.IO.Pipelines.PipeReader! reader, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -StreamJsonRpc.JsonMessageFormatter.Dispose() -> void StreamJsonRpc.JsonMessageFormatter.MultiplexingStream.get -> Nerdbank.Streams.MultiplexingStream? StreamJsonRpc.JsonMessageFormatter.MultiplexingStream.set -> void StreamJsonRpc.JsonRpc.AddRemoteRpcTarget(StreamJsonRpc.JsonRpc! remoteTarget) -> void @@ -353,7 +352,6 @@ StreamJsonRpc.JsonRpcTargetOptions.DisposeOnDisconnect.get -> bool StreamJsonRpc.JsonRpcTargetOptions.DisposeOnDisconnect.set -> void StreamJsonRpc.MessagePackFormatter StreamJsonRpc.MessagePackFormatter.Deserialize(System.Buffers.ReadOnlySequence contentBuffer) -> StreamJsonRpc.Protocol.JsonRpcMessage! -StreamJsonRpc.MessagePackFormatter.Dispose() -> void StreamJsonRpc.MessagePackFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> object! StreamJsonRpc.MessagePackFormatter.MessagePackFormatter() -> void StreamJsonRpc.MessagePackFormatter.MultiplexingStream.get -> Nerdbank.Streams.MultiplexingStream? @@ -520,9 +518,8 @@ override StreamJsonRpc.RemoteRpcException.GetObjectData(System.Runtime.Serializa static StreamJsonRpc.MessagePackFormatter.DefaultUserDataSerializationOptions.get -> MessagePack.MessagePackSerializerOptions! static StreamJsonRpc.RequestId.operator !=(StreamJsonRpc.RequestId first, StreamJsonRpc.RequestId second) -> bool static StreamJsonRpc.RequestId.operator ==(StreamJsonRpc.RequestId first, StreamJsonRpc.RequestId second) -> bool -virtual StreamJsonRpc.JsonMessageFormatter.Dispose(bool disposing) -> void +override StreamJsonRpc.JsonMessageFormatter.Dispose(bool disposing) -> void virtual StreamJsonRpc.JsonRpc.CreateExceptionFromRpcError(StreamJsonRpc.Protocol.JsonRpcRequest! request, StreamJsonRpc.Protocol.JsonRpcError! response) -> StreamJsonRpc.RemoteRpcException! -virtual StreamJsonRpc.MessagePackFormatter.Dispose(bool disposing) -> void virtual StreamJsonRpc.Reflection.MessageFormatterDuplexPipeTracker.Dispose(bool disposing) -> void override StreamJsonRpc.TargetMethod.ToString() -> string! static StreamJsonRpc.CorrelationManagerTracingStrategy.TraceState.get -> string? diff --git a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt index d25eb0e0..194078f8 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,25 @@ +StreamJsonRpc.FormatterBase +StreamJsonRpc.FormatterBase.DeserializationTracking +StreamJsonRpc.FormatterBase.DeserializationTracking.DeserializationTracking() -> void +StreamJsonRpc.FormatterBase.DeserializationTracking.DeserializationTracking(StreamJsonRpc.FormatterBase! formatter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void +StreamJsonRpc.FormatterBase.DeserializationTracking.Dispose() -> void +StreamJsonRpc.FormatterBase.DeserializingMessageWithId.get -> StreamJsonRpc.RequestId +StreamJsonRpc.FormatterBase.Dispose() -> void +StreamJsonRpc.FormatterBase.DuplexPipeTracker.get -> StreamJsonRpc.Reflection.MessageFormatterDuplexPipeTracker! +StreamJsonRpc.FormatterBase.EnumerableTracker.get -> StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker! +StreamJsonRpc.FormatterBase.FormatterBase() -> void +StreamJsonRpc.FormatterBase.FormatterProgressTracker.get -> StreamJsonRpc.Reflection.MessageFormatterProgressTracker! +StreamJsonRpc.FormatterBase.JsonRpc.get -> StreamJsonRpc.JsonRpc? +StreamJsonRpc.FormatterBase.MultiplexingStream.get -> Nerdbank.Streams.MultiplexingStream? +StreamJsonRpc.FormatterBase.MultiplexingStream.set -> void +StreamJsonRpc.FormatterBase.SerializationTracking +StreamJsonRpc.FormatterBase.SerializationTracking.Dispose() -> void +StreamJsonRpc.FormatterBase.SerializationTracking.SerializationTracking() -> void +StreamJsonRpc.FormatterBase.SerializationTracking.SerializationTracking(StreamJsonRpc.FormatterBase! formatter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void +StreamJsonRpc.FormatterBase.SerializingMessageWithId.get -> StreamJsonRpc.RequestId +StreamJsonRpc.FormatterBase.SerializingRequest.get -> bool +StreamJsonRpc.FormatterBase.TrackDeserialization(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> StreamJsonRpc.FormatterBase.DeserializationTracking +StreamJsonRpc.FormatterBase.TrackSerialization(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> StreamJsonRpc.FormatterBase.SerializationTracking StreamJsonRpc.SystemTextJsonFormatter StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequence contentBuffer) -> StreamJsonRpc.Protocol.JsonRpcMessage! StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequence contentBuffer, System.Text.Encoding! encoding) -> StreamJsonRpc.Protocol.JsonRpcMessage! @@ -6,4 +28,5 @@ StreamJsonRpc.SystemTextJsonFormatter.Encoding.set -> void StreamJsonRpc.SystemTextJsonFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> object! StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.set -> void StreamJsonRpc.SystemTextJsonFormatter.Serialize(System.Buffers.IBufferWriter! bufferWriter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void -StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void \ No newline at end of file +StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void +virtual StreamJsonRpc.FormatterBase.Dispose(bool disposing) -> void \ No newline at end of file diff --git a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Shipped.txt b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Shipped.txt index 7b9d6004..b5cf3455 100644 --- a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Shipped.txt +++ b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Shipped.txt @@ -305,7 +305,6 @@ StreamJsonRpc.IJsonRpcInstanceContainer StreamJsonRpc.IJsonRpcInstanceContainer.Rpc.set -> void StreamJsonRpc.JsonMessageFormatter.DeserializeAsync(System.IO.Pipelines.PipeReader! reader, System.Text.Encoding! encoding, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask StreamJsonRpc.JsonMessageFormatter.DeserializeAsync(System.IO.Pipelines.PipeReader! reader, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -StreamJsonRpc.JsonMessageFormatter.Dispose() -> void StreamJsonRpc.JsonMessageFormatter.MultiplexingStream.get -> Nerdbank.Streams.MultiplexingStream? StreamJsonRpc.JsonMessageFormatter.MultiplexingStream.set -> void StreamJsonRpc.JsonRpc.AddRemoteRpcTarget(StreamJsonRpc.JsonRpc! remoteTarget) -> void @@ -353,7 +352,6 @@ StreamJsonRpc.JsonRpcTargetOptions.DisposeOnDisconnect.get -> bool StreamJsonRpc.JsonRpcTargetOptions.DisposeOnDisconnect.set -> void StreamJsonRpc.MessagePackFormatter StreamJsonRpc.MessagePackFormatter.Deserialize(System.Buffers.ReadOnlySequence contentBuffer) -> StreamJsonRpc.Protocol.JsonRpcMessage! -StreamJsonRpc.MessagePackFormatter.Dispose() -> void StreamJsonRpc.MessagePackFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> object! StreamJsonRpc.MessagePackFormatter.MessagePackFormatter() -> void StreamJsonRpc.MessagePackFormatter.MultiplexingStream.get -> Nerdbank.Streams.MultiplexingStream? @@ -520,9 +518,8 @@ override StreamJsonRpc.RemoteRpcException.GetObjectData(System.Runtime.Serializa static StreamJsonRpc.MessagePackFormatter.DefaultUserDataSerializationOptions.get -> MessagePack.MessagePackSerializerOptions! static StreamJsonRpc.RequestId.operator !=(StreamJsonRpc.RequestId first, StreamJsonRpc.RequestId second) -> bool static StreamJsonRpc.RequestId.operator ==(StreamJsonRpc.RequestId first, StreamJsonRpc.RequestId second) -> bool -virtual StreamJsonRpc.JsonMessageFormatter.Dispose(bool disposing) -> void +override StreamJsonRpc.JsonMessageFormatter.Dispose(bool disposing) -> void virtual StreamJsonRpc.JsonRpc.CreateExceptionFromRpcError(StreamJsonRpc.Protocol.JsonRpcRequest! request, StreamJsonRpc.Protocol.JsonRpcError! response) -> StreamJsonRpc.RemoteRpcException! -virtual StreamJsonRpc.MessagePackFormatter.Dispose(bool disposing) -> void virtual StreamJsonRpc.Reflection.MessageFormatterDuplexPipeTracker.Dispose(bool disposing) -> void override StreamJsonRpc.TargetMethod.ToString() -> string! static StreamJsonRpc.CorrelationManagerTracingStrategy.TraceState.get -> string? diff --git a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt index d25eb0e0..194078f8 100644 --- a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt @@ -1,3 +1,25 @@ +StreamJsonRpc.FormatterBase +StreamJsonRpc.FormatterBase.DeserializationTracking +StreamJsonRpc.FormatterBase.DeserializationTracking.DeserializationTracking() -> void +StreamJsonRpc.FormatterBase.DeserializationTracking.DeserializationTracking(StreamJsonRpc.FormatterBase! formatter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void +StreamJsonRpc.FormatterBase.DeserializationTracking.Dispose() -> void +StreamJsonRpc.FormatterBase.DeserializingMessageWithId.get -> StreamJsonRpc.RequestId +StreamJsonRpc.FormatterBase.Dispose() -> void +StreamJsonRpc.FormatterBase.DuplexPipeTracker.get -> StreamJsonRpc.Reflection.MessageFormatterDuplexPipeTracker! +StreamJsonRpc.FormatterBase.EnumerableTracker.get -> StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker! +StreamJsonRpc.FormatterBase.FormatterBase() -> void +StreamJsonRpc.FormatterBase.FormatterProgressTracker.get -> StreamJsonRpc.Reflection.MessageFormatterProgressTracker! +StreamJsonRpc.FormatterBase.JsonRpc.get -> StreamJsonRpc.JsonRpc? +StreamJsonRpc.FormatterBase.MultiplexingStream.get -> Nerdbank.Streams.MultiplexingStream? +StreamJsonRpc.FormatterBase.MultiplexingStream.set -> void +StreamJsonRpc.FormatterBase.SerializationTracking +StreamJsonRpc.FormatterBase.SerializationTracking.Dispose() -> void +StreamJsonRpc.FormatterBase.SerializationTracking.SerializationTracking() -> void +StreamJsonRpc.FormatterBase.SerializationTracking.SerializationTracking(StreamJsonRpc.FormatterBase! formatter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void +StreamJsonRpc.FormatterBase.SerializingMessageWithId.get -> StreamJsonRpc.RequestId +StreamJsonRpc.FormatterBase.SerializingRequest.get -> bool +StreamJsonRpc.FormatterBase.TrackDeserialization(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> StreamJsonRpc.FormatterBase.DeserializationTracking +StreamJsonRpc.FormatterBase.TrackSerialization(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> StreamJsonRpc.FormatterBase.SerializationTracking StreamJsonRpc.SystemTextJsonFormatter StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequence contentBuffer) -> StreamJsonRpc.Protocol.JsonRpcMessage! StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequence contentBuffer, System.Text.Encoding! encoding) -> StreamJsonRpc.Protocol.JsonRpcMessage! @@ -6,4 +28,5 @@ StreamJsonRpc.SystemTextJsonFormatter.Encoding.set -> void StreamJsonRpc.SystemTextJsonFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> object! StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.set -> void StreamJsonRpc.SystemTextJsonFormatter.Serialize(System.Buffers.IBufferWriter! bufferWriter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void -StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void \ No newline at end of file +StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void +virtual StreamJsonRpc.FormatterBase.Dispose(bool disposing) -> void \ No newline at end of file From 603682bbb7d72296cc57bd12a7f18caa01bb5b31 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 19 Apr 2023 16:17:08 -0600 Subject: [PATCH 11/35] Share more code across formatters --- src/StreamJsonRpc/FormatterBase.cs | 33 +++++++++++++++++++++ src/StreamJsonRpc/JsonMessageFormatter.cs | 35 ++++------------------- src/StreamJsonRpc/MessagePackFormatter.cs | 25 +--------------- 3 files changed, 39 insertions(+), 54 deletions(-) diff --git a/src/StreamJsonRpc/FormatterBase.cs b/src/StreamJsonRpc/FormatterBase.cs index e0fc4271..b4bfac1d 100644 --- a/src/StreamJsonRpc/FormatterBase.cs +++ b/src/StreamJsonRpc/FormatterBase.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics; using System.IO.Pipelines; using Nerdbank.Streams; using StreamJsonRpc.Protocol; @@ -183,6 +184,38 @@ protected virtual void Dispose(bool disposing) /// A value to dispose of when serialization has completed. protected SerializationTracking TrackSerialization(JsonRpcMessage message) => new(this, message); + private protected void TryHandleSpecialIncomingMessage(JsonRpcMessage message) + { + switch (message) + { + case JsonRpcRequest request: + // If method is $/progress, get the progress instance from the dictionary and call Report + if (this.JsonRpc is not null && string.Equals(request.Method, MessageFormatterProgressTracker.ProgressRequestSpecialMethod, StringComparison.Ordinal)) + { + try + { + if (request.TryGetArgumentByNameOrIndex("token", 0, typeof(long), out object? tokenObject) && tokenObject is long progressId) + { + MessageFormatterProgressTracker.ProgressParamInformation? progressInfo = null; + if (this.FormatterProgressTracker.TryGetProgressObject(progressId, out progressInfo)) + { + if (request.TryGetArgumentByNameOrIndex("value", 1, progressInfo.ValueType, out object? value)) + { + progressInfo.InvokeReport(value); + } + } + } + } + catch (Exception ex) + { + this.JsonRpc.TraceSource.TraceData(TraceEventType.Error, (int)JsonRpc.TraceEvents.ProgressNotificationError, ex); + } + } + + break; + } + } + /// /// Tracks deserialization of a message. /// diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index 69d85095..e61985a9 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -608,36 +608,7 @@ private JsonRpcRequest ReadRequest(JToken json) // If method is $/progress, get the progress instance from the dictionary and call Report string? method = json.Value("method"); - if (this.JsonRpc is not null && string.Equals(method, MessageFormatterProgressTracker.ProgressRequestSpecialMethod, StringComparison.Ordinal)) - { - try - { - JToken? progressId = - args is JObject ? args["token"] : - args is JArray ? args[0] : - null; - - JToken? value = - args is JObject ? args["value"] : - args is JArray ? args[1] : - null; - - MessageFormatterProgressTracker.ProgressParamInformation? progressInfo = null; - if (progressId is object && this.FormatterProgressTracker.TryGetProgressObject(progressId.Value(), out progressInfo)) - { - object? typedValue = value?.ToObject(progressInfo.ValueType, this.JsonSerializer); - progressInfo.InvokeReport(typedValue); - } - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception e) -#pragma warning restore CA1031 // Do not catch general exception types - { - this.JsonRpc.TraceSource.TraceData(TraceEventType.Error, (int)JsonRpc.TraceEvents.ProgressNotificationError, e); - } - } - - return new JsonRpcRequest(this) + JsonRpcRequest request = new(this) { RequestId = id, Method = json.Value("method"), @@ -646,6 +617,10 @@ private JsonRpcRequest ReadRequest(JToken json) TraceState = json.Value("tracestate"), TopLevelPropertyBag = new TopLevelPropertyBag(this.JsonSerializer, (JObject)json), }; + + this.TryHandleSpecialIncomingMessage(request); + + return request; } private JsonRpcResult ReadResult(JToken json) diff --git a/src/StreamJsonRpc/MessagePackFormatter.cs b/src/StreamJsonRpc/MessagePackFormatter.cs index 8668f08b..858b7496 100644 --- a/src/StreamJsonRpc/MessagePackFormatter.cs +++ b/src/StreamJsonRpc/MessagePackFormatter.cs @@ -1644,30 +1644,7 @@ public Protocol.JsonRpcRequest Deserialize(ref MessagePackReader reader, Message result.TopLevelPropertyBag = new TopLevelPropertyBag(this.formatter.userDataSerializationOptions, topLevelProperties); } - // If method is $/progress, get the progress instance from the dictionary and call Report - if (string.Equals(result.Method, MessageFormatterProgressTracker.ProgressRequestSpecialMethod, StringComparison.Ordinal)) - { - try - { - if (result.TryGetArgumentByNameOrIndex("token", 0, typeof(long), out object? tokenObject) && tokenObject is long progressId) - { - MessageFormatterProgressTracker.ProgressParamInformation? progressInfo = null; - if (this.formatter.FormatterProgressTracker.TryGetProgressObject(progressId, out progressInfo)) - { - if (result.TryGetArgumentByNameOrIndex("value", 1, progressInfo.ValueType, out object? value)) - { - progressInfo.InvokeReport(value); - } - } - } - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types - { - this.formatter.JsonRpc?.TraceSource.TraceData(TraceEventType.Error, (int)JsonRpc.TraceEvents.ProgressNotificationError, ex); - } - } + this.formatter.TryHandleSpecialIncomingMessage(result); reader.Depth--; return result; From 78d0eef48fc5006899eebcdca5f5dda4c9ec7999 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 19 Apr 2023 17:27:13 -0600 Subject: [PATCH 12/35] Share more state of incoming messages --- src/StreamJsonRpc/FormatterBase.cs | 39 +++++++++++++------ src/StreamJsonRpc/JsonMessageFormatter.cs | 30 +++----------- src/StreamJsonRpc/MessagePackFormatter.cs | 27 +++---------- .../MessageFormatterProgressTracker.cs | 17 +++++++- .../netstandard2.0/PublicAPI.Unshipped.txt | 7 +++- .../netstandard2.1/PublicAPI.Unshipped.txt | 7 +++- 6 files changed, 66 insertions(+), 61 deletions(-) diff --git a/src/StreamJsonRpc/FormatterBase.cs b/src/StreamJsonRpc/FormatterBase.cs index b4bfac1d..27cccb39 100644 --- a/src/StreamJsonRpc/FormatterBase.cs +++ b/src/StreamJsonRpc/FormatterBase.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO.Pipelines; +using System.Reflection; using Nerdbank.Streams; using StreamJsonRpc.Protocol; using StreamJsonRpc.Reflection; @@ -134,6 +135,11 @@ protected MessageFormatterEnumerableTracker EnumerableTracker } } + /// + /// Gets the that is present on the method that may be invoked to serve the incoming request, when applicable. + /// + protected JsonRpcMethodAttribute? ApplicableMethodAttributeOnDeserializingMethod { get; private set; } + /// /// Gets the helper for marshaling in RPC method arguments or return values. /// @@ -173,9 +179,9 @@ protected virtual void Dispose(bool disposing) /// /// Sets up state to track deserialization of a message. /// - /// The object being deserialized. /// A value to dispose of when deserialization has completed. - protected DeserializationTracking TrackDeserialization(JsonRpcMessage message) => new(this, message); + /// + protected DeserializationTracking TrackDeserialization(JsonRpcMessage message, ReadOnlySpan parameters = default) => new(this, message, parameters); /// /// Sets up state to track serialization of a message. @@ -189,7 +195,7 @@ private protected void TryHandleSpecialIncomingMessage(JsonRpcMessage message) switch (message) { case JsonRpcRequest request: - // If method is $/progress, get the progress instance from the dictionary and call Report + // If method is $/progress, get the progress instance from the dictionary and call Report. if (this.JsonRpc is not null && string.Equals(request.Method, MessageFormatterProgressTracker.ProgressRequestSpecialMethod, StringComparison.Ordinal)) { try @@ -221,21 +227,28 @@ private protected void TryHandleSpecialIncomingMessage(JsonRpcMessage message) /// public struct DeserializationTracking : IDisposable { - private readonly FormatterBase formatter; + private readonly FormatterBase? formatter; /// /// Initializes a new instance of the struct. /// /// The formatter. /// The message being deserialized. - public DeserializationTracking(FormatterBase formatter, JsonRpcMessage message) + /// The signature of the method that will be invoked for the incoming request, if applicable. + public DeserializationTracking(FormatterBase formatter, JsonRpcMessage message, ReadOnlySpan parameters) { // Deserialization of messages should never occur concurrently for a single instance of a formatter. - Assumes.True(formatter.DeserializingMessageWithId.IsEmpty); + // But we may be nested in another, in which case, this should do nothing. + if (formatter.DeserializingMessageWithId.IsEmpty) + { + formatter.DeserializingMessage = message; + formatter.DeserializingMessageWithId = (message as IJsonRpcMessageWithId)?.RequestId ?? default; - this.formatter = formatter; - this.formatter.DeserializingMessage = message; - this.formatter.DeserializingMessageWithId = (message as IJsonRpcMessageWithId)?.RequestId ?? default; + // Consider the attribute applied to the particular overload that we're considering right now. + formatter.ApplicableMethodAttributeOnDeserializingMethod = message is JsonRpcRequest { Method: not null } request ? formatter.JsonRpc?.GetJsonRpcMethodAttribute(request.Method, parameters) : null; + + this.formatter = formatter; + } } /// @@ -243,8 +256,12 @@ public DeserializationTracking(FormatterBase formatter, JsonRpcMessage message) /// public void Dispose() { - this.formatter.DeserializingMessageWithId = default; - this.formatter.DeserializingMessage = null; + if (this.formatter is not null) + { + this.formatter.DeserializingMessageWithId = default; + this.formatter.DeserializingMessage = null; + this.formatter.ApplicableMethodAttributeOnDeserializingMethod = null; + } } } diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index e61985a9..63ab8395 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -605,9 +605,6 @@ private JsonRpcRequest ReadRequest(JToken json) args is JArray argsArray ? (object)PartiallyParsePositionalArguments(argsArray) : null; - // If method is $/progress, get the progress instance from the dictionary and call Report - string? method = json.Value("method"); - JsonRpcRequest request = new(this) { RequestId = id, @@ -774,13 +771,9 @@ internal JsonRpcRequest(JsonMessageFormatter formatter) [IgnoreDataMember] public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - internal JsonRpcMethodAttribute? ApplicableMethodAttribute { get; private set; } - public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan parameters, Span typedArguments) { - // Consider the attribute applied to the particular overload that we're considering right now. - this.ApplicableMethodAttribute = this.Method is not null ? this.formatter.JsonRpc?.GetJsonRpcMethodAttribute(this.Method, parameters) : null; - try + using (this.formatter.TrackDeserialization(this, parameters)) { if (parameters.Length == 1 && this.NamedArguments is not null) { @@ -800,7 +793,7 @@ public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan property in this.NamedArguments) @@ -808,10 +801,7 @@ public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan MessageFormatterProgressTracker.IsSupportedProgressType(objectType); + public override bool CanConvert(Type objectType) => MessageFormatterProgressTracker.CanSerialize(objectType); public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { @@ -1060,10 +1045,7 @@ public JsonProgressServerConverter(JsonMessageFormatter formatter) this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); } - public override bool CanConvert(Type objectType) - { - return objectType.IsConstructedGenericType && objectType.GetGenericTypeDefinition().Equals(typeof(IProgress<>)); - } + public override bool CanConvert(Type objectType) => MessageFormatterProgressTracker.CanDeserialize(objectType); public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { @@ -1074,7 +1056,7 @@ public override bool CanConvert(Type objectType) Assumes.NotNull(this.formatter.JsonRpc); JToken token = JToken.Load(reader); - bool clientRequiresNamedArgs = this.formatter.DeserializingMessage is JsonRpcRequest { ApplicableMethodAttribute: { ClientRequiresNamedArguments: true } }; + bool clientRequiresNamedArgs = this.formatter.ApplicableMethodAttributeOnDeserializingMethod?.ClientRequiresNamedArguments is true; return this.formatter.FormatterProgressTracker.CreateProgress(this.formatter.JsonRpc, token, objectType, clientRequiresNamedArgs); } diff --git a/src/StreamJsonRpc/MessagePackFormatter.cs b/src/StreamJsonRpc/MessagePackFormatter.cs index 858b7496..c2507268 100644 --- a/src/StreamJsonRpc/MessagePackFormatter.cs +++ b/src/StreamJsonRpc/MessagePackFormatter.cs @@ -818,11 +818,11 @@ internal ProgressFormatterResolver(MessagePackFormatter formatter) { if (!this.progressFormatters.TryGetValue(typeof(T), out IMessagePackFormatter? formatter)) { - if (typeof(T).IsConstructedGenericType && typeof(T).GetGenericTypeDefinition().Equals(typeof(IProgress<>))) + if (MessageFormatterProgressTracker.CanDeserialize(typeof(T))) { formatter = new PreciseTypeFormatter(this.mainFormatter); } - else if (MessageFormatterProgressTracker.IsSupportedProgressType(typeof(T))) + else if (MessageFormatterProgressTracker.CanSerialize(typeof(T))) { formatter = new ProgressClientFormatter(this.mainFormatter); } @@ -889,7 +889,7 @@ public TClass Deserialize(ref MessagePackReader reader, MessagePackSerializerOpt Assumes.NotNull(this.formatter.JsonRpc); RawMessagePack token = RawMessagePack.ReadRaw(ref reader, copy: true); - bool clientRequiresNamedArgs = this.formatter.DeserializingMessage is JsonRpcRequest { ApplicableMethodAttribute: { ClientRequiresNamedArguments: true } }; + bool clientRequiresNamedArgs = this.formatter.ApplicableMethodAttributeOnDeserializingMethod?.ClientRequiresNamedArguments is true; return (TClass)this.formatter.FormatterProgressTracker.CreateProgress(this.formatter.JsonRpc, token, typeof(TClass), clientRequiresNamedArgs); } @@ -2220,8 +2220,6 @@ internal JsonRpcRequest(MessagePackFormatter formatter) internal IReadOnlyList>? MsgPackPositionalArguments { get; set; } - internal JsonRpcMethodAttribute? ApplicableMethodAttribute { get; private set; } - void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message) { Assumes.True(message == this); @@ -2236,24 +2234,16 @@ void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan parameters, Span typedArguments) { - // Consider the attribute applied to the particular overload that we're considering right now. - this.ApplicableMethodAttribute = this.Method is not null ? this.formatter.JsonRpc?.GetJsonRpcMethodAttribute(this.Method, parameters) : null; - try + using (this.formatter.TrackDeserialization(this, parameters)) { if (parameters.Length == 1 && this.MsgPackNamedArguments is not null) { - Assumes.NotNull(this.Method); - - if (this.ApplicableMethodAttribute?.UseSingleObjectParameterDeserialization ?? false) + if (this.formatter.ApplicableMethodAttributeOnDeserializingMethod?.UseSingleObjectParameterDeserialization ?? false) { var reader = new MessagePackReader(this.MsgPackArguments); try { - using (this.formatter.TrackDeserialization(this)) - { - typedArguments[0] = MessagePackSerializer.Deserialize(parameters[0].ParameterType, ref reader, this.formatter.userDataSerializationOptions); - } - + typedArguments[0] = MessagePackSerializer.Deserialize(parameters[0].ParameterType, ref reader, this.formatter.userDataSerializationOptions); return ArgumentMatchResult.Success; } catch (MessagePackSerializationException) @@ -2265,11 +2255,6 @@ public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan /// The type which may implement . /// true if given implements ; otherwise, false. - public static bool IsSupportedProgressType(Type objectType) => TrackerHelpers>.CanSerialize(objectType); + [Obsolete($"Use {nameof(CanSerialize)} instead.")] + public static bool IsSupportedProgressType(Type objectType) => CanSerialize(objectType); + + /// + /// Checks if a given implements . + /// + /// The type which may implement . + /// true if given implements ; otherwise, false. + public static bool CanSerialize(Type objectType) => TrackerHelpers>.CanSerialize(objectType); + + /// + /// Checks if a given is a closed generic of . + /// + /// The type which may be . + /// true if given is ; otherwise, false. + public static bool CanDeserialize(Type objectType) => TrackerHelpers>.CanDeserialize(objectType); /// /// Gets a type token to use as replacement of an implementing in the JSON message. diff --git a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt index 194078f8..a4fe3cab 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,7 +1,10 @@ +static StreamJsonRpc.Reflection.MessageFormatterProgressTracker.CanDeserialize(System.Type! objectType) -> bool +static StreamJsonRpc.Reflection.MessageFormatterProgressTracker.CanSerialize(System.Type! objectType) -> bool StreamJsonRpc.FormatterBase +StreamJsonRpc.FormatterBase.ApplicableMethodAttributeOnDeserializingMethod.get -> StreamJsonRpc.JsonRpcMethodAttribute? StreamJsonRpc.FormatterBase.DeserializationTracking StreamJsonRpc.FormatterBase.DeserializationTracking.DeserializationTracking() -> void -StreamJsonRpc.FormatterBase.DeserializationTracking.DeserializationTracking(StreamJsonRpc.FormatterBase! formatter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void +StreamJsonRpc.FormatterBase.DeserializationTracking.DeserializationTracking(StreamJsonRpc.FormatterBase! formatter, StreamJsonRpc.Protocol.JsonRpcMessage! message, System.ReadOnlySpan parameters) -> void StreamJsonRpc.FormatterBase.DeserializationTracking.Dispose() -> void StreamJsonRpc.FormatterBase.DeserializingMessageWithId.get -> StreamJsonRpc.RequestId StreamJsonRpc.FormatterBase.Dispose() -> void @@ -18,7 +21,7 @@ StreamJsonRpc.FormatterBase.SerializationTracking.SerializationTracking() -> voi StreamJsonRpc.FormatterBase.SerializationTracking.SerializationTracking(StreamJsonRpc.FormatterBase! formatter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void StreamJsonRpc.FormatterBase.SerializingMessageWithId.get -> StreamJsonRpc.RequestId StreamJsonRpc.FormatterBase.SerializingRequest.get -> bool -StreamJsonRpc.FormatterBase.TrackDeserialization(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> StreamJsonRpc.FormatterBase.DeserializationTracking +StreamJsonRpc.FormatterBase.TrackDeserialization(StreamJsonRpc.Protocol.JsonRpcMessage! message, System.ReadOnlySpan parameters = default(System.ReadOnlySpan)) -> StreamJsonRpc.FormatterBase.DeserializationTracking StreamJsonRpc.FormatterBase.TrackSerialization(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> StreamJsonRpc.FormatterBase.SerializationTracking StreamJsonRpc.SystemTextJsonFormatter StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequence contentBuffer) -> StreamJsonRpc.Protocol.JsonRpcMessage! diff --git a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt index 194078f8..a4fe3cab 100644 --- a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt @@ -1,7 +1,10 @@ +static StreamJsonRpc.Reflection.MessageFormatterProgressTracker.CanDeserialize(System.Type! objectType) -> bool +static StreamJsonRpc.Reflection.MessageFormatterProgressTracker.CanSerialize(System.Type! objectType) -> bool StreamJsonRpc.FormatterBase +StreamJsonRpc.FormatterBase.ApplicableMethodAttributeOnDeserializingMethod.get -> StreamJsonRpc.JsonRpcMethodAttribute? StreamJsonRpc.FormatterBase.DeserializationTracking StreamJsonRpc.FormatterBase.DeserializationTracking.DeserializationTracking() -> void -StreamJsonRpc.FormatterBase.DeserializationTracking.DeserializationTracking(StreamJsonRpc.FormatterBase! formatter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void +StreamJsonRpc.FormatterBase.DeserializationTracking.DeserializationTracking(StreamJsonRpc.FormatterBase! formatter, StreamJsonRpc.Protocol.JsonRpcMessage! message, System.ReadOnlySpan parameters) -> void StreamJsonRpc.FormatterBase.DeserializationTracking.Dispose() -> void StreamJsonRpc.FormatterBase.DeserializingMessageWithId.get -> StreamJsonRpc.RequestId StreamJsonRpc.FormatterBase.Dispose() -> void @@ -18,7 +21,7 @@ StreamJsonRpc.FormatterBase.SerializationTracking.SerializationTracking() -> voi StreamJsonRpc.FormatterBase.SerializationTracking.SerializationTracking(StreamJsonRpc.FormatterBase! formatter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void StreamJsonRpc.FormatterBase.SerializingMessageWithId.get -> StreamJsonRpc.RequestId StreamJsonRpc.FormatterBase.SerializingRequest.get -> bool -StreamJsonRpc.FormatterBase.TrackDeserialization(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> StreamJsonRpc.FormatterBase.DeserializationTracking +StreamJsonRpc.FormatterBase.TrackDeserialization(StreamJsonRpc.Protocol.JsonRpcMessage! message, System.ReadOnlySpan parameters = default(System.ReadOnlySpan)) -> StreamJsonRpc.FormatterBase.DeserializationTracking StreamJsonRpc.FormatterBase.TrackSerialization(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> StreamJsonRpc.FormatterBase.SerializationTracking StreamJsonRpc.SystemTextJsonFormatter StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequence contentBuffer) -> StreamJsonRpc.Protocol.JsonRpcMessage! From 47aeaff83d88c84e7c35472696a5d6abd553cfc8 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 19 Apr 2023 18:34:57 -0600 Subject: [PATCH 13/35] Add support for `Progress` --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 263 ++++++++++++------- 1 file changed, 165 insertions(+), 98 deletions(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 9e83cbea..ace1ac7d 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -18,7 +18,7 @@ namespace StreamJsonRpc; /// /// A formatter that emits UTF-8 encoded JSON where user data should be serializable via the . /// -public partial class SystemTextJsonFormatter : IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter +public partial class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcFormatterState { private static readonly JsonWriterOptions WriterOptions = new() { }; @@ -53,7 +53,7 @@ public SystemTextJsonFormatter() IncludeFields = true, // Provide compatibility with DataContractSerializer attributes by default. - TypeInfoResolver = DataContractResolver.OnlyDecoratedTypes, + TypeInfoResolver = new DataContractResolver(onlyRecognizeDecoratedTypes: true), }); } @@ -89,7 +89,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding throw new JsonException("Expected a JSON object at the root of the message."); } - JsonRpcMessage returnValue; + JsonRpcMessage message; if (document.RootElement.TryGetProperty(Utf8Strings.method, out JsonElement methodElement)) { JsonRpcRequest request = new(this) @@ -98,7 +98,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding Method = methodElement.GetString(), JsonArguments = document.RootElement.TryGetProperty(Utf8Strings.@params, out JsonElement paramsElement) ? paramsElement : null, }; - returnValue = request; + message = request; } else if (document.RootElement.TryGetProperty(Utf8Strings.result, out JsonElement resultElement)) { @@ -107,7 +107,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding RequestId = ReadRequestId(), JsonResult = resultElement, }; - returnValue = result; + message = result; } else if (document.RootElement.TryGetProperty(Utf8Strings.error, out JsonElement errorElement)) { @@ -122,7 +122,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding }, }; - returnValue = error; + message = error; } else { @@ -131,22 +131,24 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding if (document.RootElement.TryGetProperty(Utf8Strings.jsonrpc, out JsonElement jsonRpcElement)) { - returnValue.Version = jsonRpcElement.ValueEquals(Utf8Strings.v2_0) ? "2.0" : (jsonRpcElement.GetString() ?? throw new JsonException("Unexpected null value for jsonrpc property.")); + message.Version = jsonRpcElement.ValueEquals(Utf8Strings.v2_0) ? "2.0" : (jsonRpcElement.GetString() ?? throw new JsonException("Unexpected null value for jsonrpc property.")); } else { // Version 1.0 is implied when it is absent. - returnValue.Version = "1.0"; + message.Version = "1.0"; } RequestId ReadRequestId() { return document.RootElement.TryGetProperty(Utf8Strings.id, out JsonElement idElement) ? idElement.Deserialize(BuiltInSerializerOptions) - : RequestId.NotSpecified; + : RequestId.NotSpecified; } - return returnValue; + this.TryHandleSpecialIncomingMessage(message); + + return message; } /// @@ -158,121 +160,129 @@ public object GetJsonText(JsonRpcMessage message) /// public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message) { - using Utf8JsonWriter writer = new(bufferWriter, WriterOptions); - writer.WriteStartObject(); - WriteVersion(); - switch (message) - { - case Protocol.JsonRpcRequest request: - WriteId(request.RequestId); - writer.WriteString(Utf8Strings.method, request.Method); - WriteArguments(request); - writer.WriteEndObject(); - break; - case Protocol.JsonRpcResult result: - WriteId(result.RequestId); - WriteResult(result); - writer.WriteEndObject(); - break; - case Protocol.JsonRpcError error: - WriteId(error.RequestId); - WriteError(error); - writer.WriteEndObject(); - break; - default: - throw new ArgumentException("Unknown message type: " + message.GetType().Name, nameof(message)); - } - - void WriteVersion() - { - switch (message.Version) - { - case "1.0": - // The 1.0 protocol didn't include the version property at all. + using (this.TrackSerialization(message)) + { + using Utf8JsonWriter writer = new(bufferWriter, WriterOptions); + writer.WriteStartObject(); + WriteVersion(); + switch (message) + { + case Protocol.JsonRpcRequest request: + WriteId(request.RequestId); + writer.WriteString(Utf8Strings.method, request.Method); + WriteArguments(request); + writer.WriteEndObject(); break; - case "2.0": - writer.WriteString(Utf8Strings.jsonrpc, Utf8Strings.v2_0); + case Protocol.JsonRpcResult result: + WriteId(result.RequestId); + WriteResult(result); + writer.WriteEndObject(); break; - default: - writer.WriteString(Utf8Strings.jsonrpc, message.Version); + case Protocol.JsonRpcError error: + WriteId(error.RequestId); + WriteError(error); + writer.WriteEndObject(); break; + default: + throw new ArgumentException("Unknown message type: " + message.GetType().Name, nameof(message)); } - } - void WriteId(RequestId id) - { - if (!id.IsEmpty) + void WriteVersion() { - writer.WritePropertyName(Utf8Strings.id); - RequestIdJsonConverter.Instance.Write(writer, id, BuiltInSerializerOptions); + switch (message.Version) + { + case "1.0": + // The 1.0 protocol didn't include the version property at all. + break; + case "2.0": + writer.WriteString(Utf8Strings.jsonrpc, Utf8Strings.v2_0); + break; + default: + writer.WriteString(Utf8Strings.jsonrpc, message.Version); + break; + } } - } - void WriteArguments(Protocol.JsonRpcRequest request) - { - if (request.ArgumentsList is not null) + void WriteId(RequestId id) { - writer.WriteStartArray(Utf8Strings.@params); - for (int i = 0; i < request.ArgumentsList.Count; i++) + if (!id.IsEmpty) { - WriteUserData(request.ArgumentsList[i]); + writer.WritePropertyName(Utf8Strings.id); + RequestIdJsonConverter.Instance.Write(writer, id, BuiltInSerializerOptions); } - - writer.WriteEndArray(); } - else if (request.NamedArguments is not null) + + void WriteArguments(Protocol.JsonRpcRequest request) { - writer.WriteStartObject(Utf8Strings.@params); - foreach (KeyValuePair argument in request.NamedArguments) + if (request.ArgumentsList is not null) { - writer.WritePropertyName(argument.Key); - WriteUserData(argument.Value); + writer.WriteStartArray(Utf8Strings.@params); + for (int i = 0; i < request.ArgumentsList.Count; i++) + { + WriteUserData(request.ArgumentsList[i]); + } + + writer.WriteEndArray(); } + else if (request.NamedArguments is not null) + { + writer.WriteStartObject(Utf8Strings.@params); + foreach (KeyValuePair argument in request.NamedArguments) + { + writer.WritePropertyName(argument.Key); + WriteUserData(argument.Value); + } - writer.WriteEndObject(); + writer.WriteEndObject(); + } + else if (request.Arguments is not null) + { + // This is a custom named arguments object, so we'll just serialize it as-is. + writer.WritePropertyName(Utf8Strings.@params); + WriteUserData(request.Arguments); + } } - else if (request.Arguments is not null) + + void WriteResult(Protocol.JsonRpcResult result) { - // This is a custom named arguments object, so we'll just serialize it as-is. - writer.WritePropertyName(Utf8Strings.@params); - WriteUserData(request.Arguments); + writer.WritePropertyName(Utf8Strings.result); + WriteUserData(result.Result); } - } - - void WriteResult(Protocol.JsonRpcResult result) - { - writer.WritePropertyName(Utf8Strings.result); - WriteUserData(result.Result); - } - void WriteError(Protocol.JsonRpcError error) - { - if (error.Error is null) + void WriteError(Protocol.JsonRpcError error) { - throw new ArgumentException($"{nameof(error.Error)} property must be set.", nameof(message)); + if (error.Error is null) + { + throw new ArgumentException($"{nameof(error.Error)} property must be set.", nameof(message)); + } + + writer.WriteStartObject(Utf8Strings.error); + writer.WriteNumber(Utf8Strings.code, (int)error.Error.Code); + writer.WriteString(Utf8Strings.message, error.Error.Message); + if (error.Error.Data is not null) + { + writer.WritePropertyName(Utf8Strings.data); + WriteUserData(error.Error.Data); + } + + writer.WriteEndObject(); } - writer.WriteStartObject(Utf8Strings.error); - writer.WriteNumber(Utf8Strings.code, (int)error.Error.Code); - writer.WriteString(Utf8Strings.message, error.Error.Message); - if (error.Error.Data is not null) + void WriteUserData(object? value) { - writer.WritePropertyName(Utf8Strings.data); - WriteUserData(error.Error.Data); + JsonSerializer.Serialize(writer, value, this.massagedUserDataSerializerOptions); } - - writer.WriteEndObject(); - } - - void WriteUserData(object? value) - { - JsonSerializer.Serialize(writer, value, this.massagedUserDataSerializerOptions); } } private JsonSerializerOptions MassageUserDataSerializerOptions(JsonSerializerOptions options) { + // This is required for $/cancelRequest messages. options.Converters.Add(RequestIdJsonConverter.Instance); + + // Add support for exotic types. + options.Converters.Add(new ProgressConverterFactory(this)); + return options; } @@ -316,6 +326,8 @@ internal JsonRpcRequest(SystemTextJsonFormatter formatter) public override int ArgumentCount => this.argumentCount; + internal JsonRpcMethodAttribute? ApplicableMethodAttribute { get; private set; } + internal JsonElement? JsonArguments { get => this.jsonArguments; @@ -373,7 +385,10 @@ public override bool TryGetArgumentByNameOrIndex(string? name, int position, Typ try { - value = valueElement?.Deserialize(typeHint ?? typeof(object), this.formatter.massagedUserDataSerializerOptions); + using (this.formatter.TrackDeserialization(this)) + { + value = valueElement?.Deserialize(typeHint ?? typeof(object), this.formatter.massagedUserDataSerializerOptions); + } } catch (JsonException ex) { @@ -559,17 +574,69 @@ public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerialize } } - private class DataContractResolver : IJsonTypeInfoResolver + private class ProgressConverterFactory : JsonConverterFactory { - internal static readonly DataContractResolver OnlyDecoratedTypes = new(onlyRecognizeDecoratedTypes: true); + private readonly SystemTextJsonFormatter formatter; + internal ProgressConverterFactory(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override bool CanConvert(Type typeToConvert) => MessageFormatterProgressTracker.CanSerialize(typeToConvert) || MessageFormatterProgressTracker.CanDeserialize(typeToConvert); + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type progressType = typeToConvert.GetGenericArguments()[0]; + Type converterType = typeof(Converter<>).MakeGenericType(progressType); + return (JsonConverter)Activator.CreateInstance(converterType, this.formatter)!; + } + + private class Converter : JsonConverter?> + { + private readonly SystemTextJsonFormatter formatter; + + public Converter(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override IProgress? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + Assumes.NotNull(this.formatter.JsonRpc); + object token = reader.TokenType switch + { + JsonTokenType.String => reader.GetString()!, + JsonTokenType.Number => reader.GetInt64(), + _ => throw new NotSupportedException("Unsupported token type."), // Ideally, we should *copy* the token so we can retain it and replay it later. + }; + + bool clientRequiresNamedArgs = this.formatter.DeserializingMessage is JsonRpcRequest { ApplicableMethodAttribute: { ClientRequiresNamedArguments: true } }; + return (IProgress?)this.formatter.FormatterProgressTracker.CreateProgress(this.formatter.JsonRpc, token, typeToConvert, clientRequiresNamedArgs); + } + + public override void Write(Utf8JsonWriter writer, IProgress? value, JsonSerializerOptions options) + { + long progressId = this.formatter.FormatterProgressTracker.GetTokenForProgress(value!); + writer.WriteNumberValue(progressId); + } + } + } + + private class DataContractResolver : IJsonTypeInfoResolver + { private readonly Dictionary typeInfoCache = new(); private readonly bool onlyRecognizeDecoratedTypes; private readonly DefaultJsonTypeInfoResolver fallbackResolver = new(); - private DataContractResolver(bool onlyRecognizeDecoratedTypes) + internal DataContractResolver(bool onlyRecognizeDecoratedTypes) { this.onlyRecognizeDecoratedTypes = onlyRecognizeDecoratedTypes; } From e01b2aa77ea3332fd00021a8d99349639599bb1d Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 19 Apr 2023 18:45:17 -0600 Subject: [PATCH 14/35] Add support for `UseSingleObjectParameterDeserialization` --- src/StreamJsonRpc/JsonMessageFormatter.cs | 17 +++++++---------- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index 63ab8395..1e01626e 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -791,20 +791,17 @@ public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan property in this.NamedArguments) { - var obj = new JObject(); - foreach (KeyValuePair property in this.NamedArguments) - { - obj.Add(new JProperty(property.Key, property.Value)); - } + obj.Add(new JProperty(property.Key, property.Value)); + } - typedArguments[0] = obj.ToObject(parameters[0].ParameterType, this.formatter.JsonSerializer); + typedArguments[0] = obj.ToObject(parameters[0].ParameterType, this.formatter.JsonSerializer); - return ArgumentMatchResult.Success; - } + return ArgumentMatchResult.Success; } } diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index ace1ac7d..32296fd2 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -349,6 +349,22 @@ public void DeserializationComplete(JsonRpcMessage message) this.jsonArguments = null; } + public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan parameters, Span typedArguments) + { + using (this.formatter.TrackDeserialization(this, parameters)) + { + // Support for opt-in to deserializing all named arguments into a single parameter. + if (parameters.Length == 1 && this.formatter.ApplicableMethodAttributeOnDeserializingMethod?.UseSingleObjectParameterDeserialization is true) + { + Assumes.NotNull(this.JsonArguments); + typedArguments[0] = this.JsonArguments.Value.Deserialize(parameters[0].ParameterType, this.formatter.massagedUserDataSerializerOptions); + return ArgumentMatchResult.Success; + } + + return base.TryGetTypedArguments(parameters, typedArguments); + } + } + public override bool TryGetArgumentByNameOrIndex(string? name, int position, Type? typeHint, out object? value) { if (this.JsonArguments is null) From 5c755a08de2742ca0585331e8d864126e5941b87 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 19 Apr 2023 19:55:08 -0600 Subject: [PATCH 15/35] Share more tests across formatters --- test/StreamJsonRpc.Tests/FormatterTestBase.cs | 118 +++++++++++++++ .../JsonMessageFormatterTests.cs | 139 ++++-------------- .../MessagePackFormatterTests.cs | 112 ++------------ .../StreamJsonRpc.Tests.csproj | 3 + .../SystemTextJsonFormatterTests.cs | 15 ++ 5 files changed, 175 insertions(+), 212 deletions(-) create mode 100644 test/StreamJsonRpc.Tests/FormatterTestBase.cs create mode 100644 test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs diff --git a/test/StreamJsonRpc.Tests/FormatterTestBase.cs b/test/StreamJsonRpc.Tests/FormatterTestBase.cs new file mode 100644 index 00000000..92803a25 --- /dev/null +++ b/test/StreamJsonRpc.Tests/FormatterTestBase.cs @@ -0,0 +1,118 @@ +using System.Runtime.Serialization; +using MessagePack.Formatters; +using Nerdbank.Streams; +using StreamJsonRpc; +using StreamJsonRpc.Protocol; +using Xunit; +using Xunit.Abstractions; + +public abstract class FormatterTestBase : TestBase + where TFormatter : IJsonRpcMessageFormatter +{ + private TFormatter? formatter; + + protected FormatterTestBase(ITestOutputHelper logger) + : base(logger) + { + } + + protected TFormatter Formatter + { + get => this.formatter ??= this.CreateFormatter(); + set => this.formatter = value; + } + + [SkippableFact] + public void TopLevelPropertiesCanBeSerializedRequest() + { + IJsonRpcMessageFactory? factory = this.CreateFormatter() as IJsonRpcMessageFactory; + Skip.If(factory is null); + JsonRpcRequest requestMessage = factory.CreateRequestMessage(); + Assert.NotNull(requestMessage); + + requestMessage.Method = "test"; + Assert.True(requestMessage.TrySetTopLevelProperty("testProperty", "testValue")); + Assert.True(requestMessage.TrySetTopLevelProperty("objectProperty", new CustomType() { Age = 25 })); + + JsonRpcRequest roundTripMessage = this.Roundtrip(requestMessage); + Assert.True(roundTripMessage.TryGetTopLevelProperty("testProperty", out string? value)); + Assert.Equal("testValue", value); + + Assert.True(roundTripMessage.TryGetTopLevelProperty("objectProperty", out CustomType? customObject)); + Assert.Equal(25, customObject?.Age); + } + + [SkippableFact] + public void TopLevelPropertiesCanBeSerializedResult() + { + IJsonRpcMessageFactory? factory = this.CreateFormatter() as IJsonRpcMessageFactory; + Skip.If(factory is null); + var message = factory.CreateResultMessage(); + Assert.NotNull(message); + + message.Result = "test"; + Assert.True(message.TrySetTopLevelProperty("testProperty", "testValue")); + Assert.True(message.TrySetTopLevelProperty("objectProperty", new CustomType() { Age = 25 })); + + var roundTripMessage = this.Roundtrip(message); + Assert.True(roundTripMessage.TryGetTopLevelProperty("testProperty", out string? value)); + Assert.Equal("testValue", value); + + Assert.True(roundTripMessage.TryGetTopLevelProperty("objectProperty", out CustomType? customObject)); + Assert.Equal(25, customObject?.Age); + } + + [SkippableFact] + public void TopLevelPropertiesCanBeSerializedError() + { + IJsonRpcMessageFactory? factory = this.CreateFormatter() as IJsonRpcMessageFactory; + Skip.If(factory is null); + var message = factory.CreateErrorMessage(); + Assert.NotNull(message); + + message.Error = new JsonRpcError.ErrorDetail() { Message = "test" }; + Assert.True(message.TrySetTopLevelProperty("testProperty", "testValue")); + Assert.True(message.TrySetTopLevelProperty("objectProperty", new CustomType() { Age = 25 })); + + var roundTripMessage = this.Roundtrip(message); + Assert.True(roundTripMessage.TryGetTopLevelProperty("testProperty", out string? value)); + Assert.Equal("testValue", value); + + Assert.True(roundTripMessage.TryGetTopLevelProperty("objectProperty", out CustomType? customObject)); + Assert.Equal(25, customObject?.Age); + } + + [SkippableFact] + public void TopLevelPropertiesWithNullValue() + { + IJsonRpcMessageFactory? factory = this.CreateFormatter() as IJsonRpcMessageFactory; + Skip.If(factory is null); + var requestMessage = factory.CreateRequestMessage(); + Assert.NotNull(requestMessage); + + requestMessage.Method = "test"; + Assert.True(requestMessage.TrySetTopLevelProperty("testProperty", null)); + + var roundTripMessage = this.Roundtrip(requestMessage); + Assert.True(roundTripMessage.TryGetTopLevelProperty("testProperty", out string? value)); + Assert.Null(value); + } + + protected abstract TFormatter CreateFormatter(); + + protected T Roundtrip(T value) + where T : JsonRpcMessage + { + var sequence = new Sequence(); + this.Formatter.Serialize(sequence, value); + var actual = (T)this.Formatter.Deserialize(sequence); + return actual; + } + + [DataContract] + public class CustomType + { + [DataMember] + public int Age { get; set; } + } +} diff --git a/test/StreamJsonRpc.Tests/JsonMessageFormatterTests.cs b/test/StreamJsonRpc.Tests/JsonMessageFormatterTests.cs index c74ddd85..57336cd4 100644 --- a/test/StreamJsonRpc.Tests/JsonMessageFormatterTests.cs +++ b/test/StreamJsonRpc.Tests/JsonMessageFormatterTests.cs @@ -11,7 +11,7 @@ using Xunit; using Xunit.Abstractions; -public class JsonMessageFormatterTests : TestBase +public class JsonMessageFormatterTests : FormatterTestBase { public JsonMessageFormatterTests(ITestOutputHelper logger) : base(logger) @@ -21,15 +21,13 @@ public JsonMessageFormatterTests(ITestOutputHelper logger) [Fact] public void DefaultEncodingLacksPreamble() { - var formatter = new JsonMessageFormatter(); - Assert.Empty(formatter.Encoding.GetPreamble()); + Assert.Empty(this.Formatter.Encoding.GetPreamble()); } [Fact] public void ProtocolVersion_Default() { - var formatter = new JsonMessageFormatter(); - Assert.Equal(new Version(2, 0), formatter.ProtocolVersion); + Assert.Equal(new Version(2, 0), this.Formatter.ProtocolVersion); } [Theory] @@ -48,12 +46,11 @@ public void ProtocolVersion_AcceptedVersions(int major, int minor) [Fact] public void ProtocolVersion_RejectsOtherVersions() { - var formatter = new JsonMessageFormatter(); - Assert.Throws(() => formatter.ProtocolVersion = null!); - Assert.Throws(() => formatter.ProtocolVersion = new Version(0, 0)); - Assert.Throws(() => formatter.ProtocolVersion = new Version(1, 1)); - Assert.Throws(() => formatter.ProtocolVersion = new Version(2, 1)); - Assert.Throws(() => formatter.ProtocolVersion = new Version(3, 0)); + Assert.Throws(() => this.Formatter.ProtocolVersion = null!); + Assert.Throws(() => this.Formatter.ProtocolVersion = new Version(0, 0)); + Assert.Throws(() => this.Formatter.ProtocolVersion = new Version(1, 1)); + Assert.Throws(() => this.Formatter.ProtocolVersion = new Version(2, 1)); + Assert.Throws(() => this.Formatter.ProtocolVersion = new Version(3, 0)); } [Fact] @@ -91,9 +88,8 @@ public void EncodingPreambleWrittenOnlyOncePerMessage() [Fact] public void SerializerDefaults() { - var formatter = new JsonMessageFormatter(); - Assert.Equal(ConstructorHandling.AllowNonPublicDefaultConstructor, formatter.JsonSerializer.ConstructorHandling); - Assert.Equal(NullValueHandling.Ignore, formatter.JsonSerializer.NullValueHandling); + Assert.Equal(ConstructorHandling.AllowNonPublicDefaultConstructor, this.Formatter.JsonSerializer.ConstructorHandling); + Assert.Equal(NullValueHandling.Ignore, this.Formatter.JsonSerializer.NullValueHandling); } [Fact] @@ -124,26 +120,24 @@ public void JTokenParserHonorsSettingsOnSerializer() [Fact] public async Task MultiplexingStream() { - var formatter = new JsonMessageFormatter(); - Assert.Null(formatter.MultiplexingStream); + Assert.Null(this.Formatter.MultiplexingStream); Tuple streams = Nerdbank.FullDuplexStream.CreateStreams(); MultiplexingStream[] mxStreams = await Task.WhenAll( Nerdbank.Streams.MultiplexingStream.CreateAsync(streams.Item1, this.TimeoutToken), Nerdbank.Streams.MultiplexingStream.CreateAsync(streams.Item2, this.TimeoutToken)); - formatter.MultiplexingStream = mxStreams[0]; - Assert.Same(mxStreams[0], formatter.MultiplexingStream); + this.Formatter.MultiplexingStream = mxStreams[0]; + Assert.Same(mxStreams[0], this.Formatter.MultiplexingStream); - formatter.MultiplexingStream = null; - Assert.Null(formatter.MultiplexingStream); + this.Formatter.MultiplexingStream = null; + Assert.Null(this.Formatter.MultiplexingStream); } [Fact] public void ServerReturnsErrorWithNullId() { - var formatter = new JsonMessageFormatter(); - JsonRpcMessage? message = formatter.Deserialize(JObject.FromObject( + JsonRpcMessage? message = this.Formatter.Deserialize(JObject.FromObject( new { jsonrpc = "2.0", @@ -162,8 +156,7 @@ public void ServerReturnsErrorWithNullId() [Fact] public void ErrorResponseOmitsNullDataField() { - var formatter = new JsonMessageFormatter(); - JToken jtoken = formatter.Serialize(new JsonRpcError { RequestId = new RequestId(1), Error = new JsonRpcError.ErrorDetail { Code = JsonRpcErrorCode.InternalError, Message = "some error" } }); + JToken jtoken = this.Formatter.Serialize(new JsonRpcError { RequestId = new RequestId(1), Error = new JsonRpcError.ErrorDetail { Code = JsonRpcErrorCode.InternalError, Message = "some error" } }); this.Logger.WriteLine(jtoken.ToString(Formatting.Indented)); Assert.Equal((int)JsonRpcErrorCode.InternalError, jtoken["error"]!["code"]); Assert.Null(jtoken["error"]!["data"]); // we're testing for an undefined field -- not a field with a null value. @@ -172,8 +165,7 @@ public void ErrorResponseOmitsNullDataField() [Fact] public void ErrorResponseIncludesNonNullDataField() { - var formatter = new JsonMessageFormatter(); - JToken jtoken = formatter.Serialize(new JsonRpcError { RequestId = new RequestId(1), Error = new JsonRpcError.ErrorDetail { Code = JsonRpcErrorCode.InternalError, Message = "some error", Data = new { more = "info" } } }); + JToken jtoken = this.Formatter.Serialize(new JsonRpcError { RequestId = new RequestId(1), Error = new JsonRpcError.ErrorDetail { Code = JsonRpcErrorCode.InternalError, Message = "some error", Data = new { more = "info" } } }); this.Logger.WriteLine(jtoken.ToString(Formatting.Indented)); Assert.Equal((int)JsonRpcErrorCode.InternalError, jtoken["error"]!["code"]); Assert.Equal("info", jtoken["error"]!["data"]!.Value("more")); @@ -182,7 +174,6 @@ public void ErrorResponseIncludesNonNullDataField() [Fact] public void DeserializingResultWithMissingIdFails() { - var formatter = new JsonMessageFormatter(); var resultWithNoId = JObject.FromObject( new { @@ -193,14 +184,13 @@ public void DeserializingResultWithMissingIdFails() }, }, new JsonSerializer()); - var message = Assert.Throws(() => formatter.Deserialize(resultWithNoId)).InnerException?.Message; + var message = Assert.Throws(() => this.Formatter.Deserialize(resultWithNoId)).InnerException?.Message; Assert.Contains("\"id\" property missing.", message); } [Fact] public void DeserializingErrorWithMissingIdFails() { - var formatter = new JsonMessageFormatter(); var errorWithNoId = JObject.FromObject( new { @@ -212,7 +202,7 @@ public void DeserializingErrorWithMissingIdFails() }, }, new JsonSerializer()); - var message = Assert.Throws(() => formatter.Deserialize(errorWithNoId)).InnerException?.Message; + var message = Assert.Throws(() => this.Formatter.Deserialize(errorWithNoId)).InnerException?.Message; Assert.Contains("\"id\" property missing.", message); } @@ -222,103 +212,33 @@ public void DeserializingErrorWithMissingIdFails() [Fact] public void DateParseHandling_Default() { - var formatter = new JsonMessageFormatter(); - Assert.Equal(DateParseHandling.None, formatter.JsonSerializer.DateParseHandling); + Assert.Equal(DateParseHandling.None, this.Formatter.JsonSerializer.DateParseHandling); // Verify that the behavior matches the setting. string jsonRequest = @"{""jsonrpc"":""2.0"",""method"":""asdf"",""params"":[""2019-01-29T03:37:28.4433841Z""]}"; - ReadOnlySequence jsonSequence = new ReadOnlySequence(formatter.Encoding.GetBytes(jsonRequest)); - var jsonMessage = (JsonRpcRequest)formatter.Deserialize(jsonSequence); + ReadOnlySequence jsonSequence = new ReadOnlySequence(this.Formatter.Encoding.GetBytes(jsonRequest)); + var jsonMessage = (JsonRpcRequest)this.Formatter.Deserialize(jsonSequence); Assert.True(jsonMessage.TryGetArgumentByNameOrIndex(null, 0, typeof(string), out object? value)); Assert.IsType(value); Assert.Equal("2019-01-29T03:37:28.4433841Z", value); } - [Fact] - public void TopLevelPropertiesCanBeSerialized() - { - var formatter = new JsonMessageFormatter(); - IJsonRpcMessageFactory factory = formatter; - var jsonRequest = factory.CreateRequestMessage(); - Assert.NotNull(jsonRequest); - - jsonRequest.Method = "test"; - Assert.True(jsonRequest.TrySetTopLevelProperty("testProperty", "testValue")); - Assert.True(jsonRequest.TrySetTopLevelProperty("objectProperty", new CustomType() { Age = 25 })); - - var messageJsonObject = formatter.Serialize(jsonRequest); - var jsonMessage = (JsonRpcRequest)formatter.Deserialize(messageJsonObject); - - Assert.True(jsonMessage.TryGetTopLevelProperty("testProperty", out string? value)); - Assert.Equal("testValue", value); - - Assert.True(jsonMessage.TryGetTopLevelProperty("objectProperty", out CustomType? customObject)); - Assert.Equal(25, customObject?.Age); - } - - [Fact] - public void TopLevelPropertiesCanBeSerializedInError() - { - var formatter = new JsonMessageFormatter(); - IJsonRpcMessageFactory factory = formatter; - var jsonError = factory.CreateErrorMessage(); - jsonError.Error = new JsonRpcError.ErrorDetail() { Message = "test" }; - - Assert.True(jsonError.TrySetTopLevelProperty("testProperty", "testValue")); - - var messageJsonObject = formatter.Serialize(jsonError); - var jsonMessage = (JsonRpcError)formatter.Deserialize(messageJsonObject); - - Assert.True(jsonMessage.TryGetTopLevelProperty("testProperty", out string? value)); - Assert.Equal("testValue", value); - } - - [Fact] - public void TopLevelPropertiesCanBeSerializedInResult() - { - var formatter = new JsonMessageFormatter(); - IJsonRpcMessageFactory factory = formatter; - var jsonResult = factory.CreateResultMessage(); - Assert.True(jsonResult.TrySetTopLevelProperty("testProperty", "testValue")); - var messageJsonObject = formatter.Serialize(jsonResult); - var jsonMessage = (JsonRpcResult)formatter.Deserialize(messageJsonObject); - Assert.True(jsonMessage.TryGetTopLevelProperty("testProperty", out string? value)); - Assert.Equal("testValue", value); - } - - [Fact] - public void TopLevelPropertiesWithNullValue() - { - var formatter = new JsonMessageFormatter(); - IJsonRpcMessageFactory factory = formatter; - var jsonRequest = factory.CreateRequestMessage(); - Assert.NotNull(jsonRequest); - - jsonRequest.Method = "test"; - Assert.True(jsonRequest.TrySetTopLevelProperty("testProperty", null)); - - var messageJsonObject = formatter.Serialize(jsonRequest); - var jsonMessage = (JsonRpcRequest)formatter.Deserialize(messageJsonObject); - - Assert.True(jsonMessage.TryGetTopLevelProperty("testProperty", out string? value)); - Assert.Null(value); - } - [Fact] public void CustomParametersObjectWithJsonConverterProperties() { const string localPathUri = "file:///c:/foo"; - var formatter = new JsonMessageFormatter(); - JToken token = formatter.Serialize(new JsonRpcRequest { Method = "test", Arguments = new CustomTypeWithJsonConverterProperties { Uri = new Uri(localPathUri) } }); + JToken token = this.Formatter.Serialize(new JsonRpcRequest { Method = "test", Arguments = new CustomTypeWithJsonConverterProperties { Uri = new Uri(localPathUri) } }); this.Logger.WriteLine(token.ToString(Formatting.Indented)); Assert.Equal(CustomConverter.CustomPrefix + localPathUri, token["params"]![nameof(CustomTypeWithJsonConverterProperties.Uri)]!.ToString()); - token = formatter.Serialize(new JsonRpcRequest { Method = "test", Arguments = new CustomTypeWithJsonConverterProperties { } }); + token = this.Formatter.Serialize(new JsonRpcRequest { Method = "test", Arguments = new CustomTypeWithJsonConverterProperties { } }); this.Logger.WriteLine(token.ToString(Formatting.Indented)); Assert.Equal(JTokenType.Null, token["params"]![nameof(CustomTypeWithJsonConverterProperties.Uri)]!.Type); Assert.Null(token["params"]![nameof(CustomTypeWithJsonConverterProperties.UriIgnorable)]); } + protected override JsonMessageFormatter CreateFormatter() => new(); + private static long MeasureLength(JsonRpcRequest msg, JsonMessageFormatter formatter) { var builder = new Sequence(); @@ -330,11 +250,6 @@ private static long MeasureLength(JsonRpcRequest msg, JsonMessageFormatter forma return length; } - public class CustomType - { - public int Age { get; set; } - } - public class CustomTypeWithJsonConverterProperties { [JsonConverter(typeof(CustomConverter))] diff --git a/test/StreamJsonRpc.Tests/MessagePackFormatterTests.cs b/test/StreamJsonRpc.Tests/MessagePackFormatterTests.cs index 423d0368..39f63348 100644 --- a/test/StreamJsonRpc.Tests/MessagePackFormatterTests.cs +++ b/test/StreamJsonRpc.Tests/MessagePackFormatterTests.cs @@ -10,10 +10,8 @@ using Xunit; using Xunit.Abstractions; -public class MessagePackFormatterTests : TestBase +public class MessagePackFormatterTests : FormatterTestBase { - private readonly MessagePackFormatter formatter = new MessagePackFormatter(); - public MessagePackFormatterTests(ITestOutputHelper logger) : base(logger) { @@ -136,7 +134,7 @@ public async Task BasicJsonRpc() [Fact] public void Resolver_RequestArgInArray() { - this.formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new CustomFormatter()))); + this.Formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new CustomFormatter()))); var originalArg = new TypeRequiringCustomFormatter { Prop1 = 3, Prop2 = 5 }; var originalRequest = new JsonRpcRequest { @@ -154,7 +152,7 @@ public void Resolver_RequestArgInArray() [Fact] public void Resolver_RequestArgInNamedArgs_AnonymousType() { - this.formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new IMessagePackFormatter[] { new CustomFormatter() }, new IFormatterResolver[] { BuiltinResolver.Instance }))); + this.Formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new IMessagePackFormatter[] { new CustomFormatter() }, new IFormatterResolver[] { BuiltinResolver.Instance }))); var originalArg = new { Prop1 = 3, Prop2 = 5 }; var originalRequest = new JsonRpcRequest { @@ -172,7 +170,7 @@ public void Resolver_RequestArgInNamedArgs_AnonymousType() [Fact] public void Resolver_RequestArgInNamedArgs_DataContractObject() { - this.formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new IMessagePackFormatter[] { new CustomFormatter() }, new IFormatterResolver[] { BuiltinResolver.Instance }))); + this.Formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new IMessagePackFormatter[] { new CustomFormatter() }, new IFormatterResolver[] { BuiltinResolver.Instance }))); var originalArg = new DataContractWithSubsetOfMembersIncluded { ExcludedField = "A", ExcludedProperty = "B", IncludedField = "C", IncludedProperty = "D" }; var originalRequest = new JsonRpcRequest { @@ -192,7 +190,7 @@ public void Resolver_RequestArgInNamedArgs_DataContractObject() [Fact] public void Resolver_RequestArgInNamedArgs_NonDataContractObject() { - this.formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new IMessagePackFormatter[] { new CustomFormatter() }, new IFormatterResolver[] { BuiltinResolver.Instance }))); + this.Formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new IMessagePackFormatter[] { new CustomFormatter() }, new IFormatterResolver[] { BuiltinResolver.Instance }))); var originalArg = new NonDataContractWithExcludedMembers { ExcludedField = "A", ExcludedProperty = "B", InternalField = "C", InternalProperty = "D", PublicField = "E", PublicProperty = "F" }; var originalRequest = new JsonRpcRequest { @@ -228,7 +226,7 @@ public void Resolver_RequestArgInNamedArgs_NullObject() [Fact] public void Resolver_Result() { - this.formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new CustomFormatter()))); + this.Formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new CustomFormatter()))); var originalResultValue = new TypeRequiringCustomFormatter { Prop1 = 3, Prop2 = 5 }; var originalResult = new JsonRpcResult { @@ -244,7 +242,7 @@ public void Resolver_Result() [Fact] public void Resolver_ErrorData() { - this.formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new CustomFormatter()))); + this.Formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new CustomFormatter()))); var originalErrorData = new TypeRequiringCustomFormatter { Prop1 = 3, Prop2 = 5 }; var originalError = new JsonRpcError { @@ -276,7 +274,7 @@ public void ActualOptions_IsOrDerivesFrom_SetMessagePackSerializerOptions() var customFormatter = new CustomFormatter(); var options = (CustomOptions)new CustomOptions(MessagePackFormatter.DefaultUserDataSerializationOptions) { CustomProperty = 3 } .WithResolver(CompositeResolver.Create(customFormatter)); - this.formatter.SetMessagePackSerializerOptions(options); + this.Formatter.SetMessagePackSerializerOptions(options); var value = new JsonRpcRequest { RequestId = new RequestId(1), @@ -285,7 +283,7 @@ public void ActualOptions_IsOrDerivesFrom_SetMessagePackSerializerOptions() }; var sequence = new Sequence(); - this.formatter.Serialize(sequence, value); + this.Formatter.Serialize(sequence, value); var observedOptions = Assert.IsType(customFormatter.LastObservedOptions); Assert.Equal(options.CustomProperty, observedOptions.CustomProperty); @@ -341,78 +339,6 @@ public void CanDeserializeWithExtraProperty_JsonRpcError() Assert.Equal(dynamic.error.code, (int?)request.Error?.Code); } - [Fact] - public void TopLevelPropertiesCanBeSerializedRequest() - { - IJsonRpcMessageFactory factory = this.formatter; - var requestMessage = factory.CreateRequestMessage(); - Assert.NotNull(requestMessage); - - requestMessage.Method = "test"; - Assert.True(requestMessage.TrySetTopLevelProperty("testProperty", "testValue")); - Assert.True(requestMessage.TrySetTopLevelProperty("objectProperty", new CustomType() { Age = 25 })); - - var roundTripMessage = this.Roundtrip(requestMessage); - Assert.True(roundTripMessage.TryGetTopLevelProperty("testProperty", out string? value)); - Assert.Equal("testValue", value); - - Assert.True(roundTripMessage.TryGetTopLevelProperty("objectProperty", out CustomType? customObject)); - Assert.Equal(25, customObject?.Age); - } - - [Fact] - public void TopLevelPropertiesCanBeSerializedResult() - { - IJsonRpcMessageFactory factory = this.formatter; - var message = factory.CreateResultMessage(); - Assert.NotNull(message); - - message.Result = "test"; - Assert.True(message.TrySetTopLevelProperty("testProperty", "testValue")); - Assert.True(message.TrySetTopLevelProperty("objectProperty", new CustomType() { Age = 25 })); - - var roundTripMessage = this.Roundtrip(message); - Assert.True(roundTripMessage.TryGetTopLevelProperty("testProperty", out string? value)); - Assert.Equal("testValue", value); - - Assert.True(roundTripMessage.TryGetTopLevelProperty("objectProperty", out CustomType? customObject)); - Assert.Equal(25, customObject?.Age); - } - - [Fact] - public void TopLevelPropertiesCanBeSerializedError() - { - IJsonRpcMessageFactory factory = this.formatter; - var message = factory.CreateErrorMessage(); - Assert.NotNull(message); - - message.Error = new JsonRpcError.ErrorDetail() { Message = "test" }; - Assert.True(message.TrySetTopLevelProperty("testProperty", "testValue")); - Assert.True(message.TrySetTopLevelProperty("objectProperty", new CustomType() { Age = 25 })); - - var roundTripMessage = this.Roundtrip(message); - Assert.True(roundTripMessage.TryGetTopLevelProperty("testProperty", out string? value)); - Assert.Equal("testValue", value); - - Assert.True(roundTripMessage.TryGetTopLevelProperty("objectProperty", out CustomType? customObject)); - Assert.Equal(25, customObject?.Age); - } - - [Fact] - public void TopLevelPropertiesWithNullValue() - { - IJsonRpcMessageFactory factory = this.formatter; - var requestMessage = factory.CreateRequestMessage(); - Assert.NotNull(requestMessage); - - requestMessage.Method = "test"; - Assert.True(requestMessage.TrySetTopLevelProperty("testProperty", null)); - - var roundTripMessage = this.Roundtrip(requestMessage); - Assert.True(roundTripMessage.TryGetTopLevelProperty("testProperty", out string? value)); - Assert.Null(value); - } - [Fact] public void StringsInUserDataAreInterned() { @@ -445,6 +371,8 @@ public void StringValuesOfStandardPropertiesAreInterned() Assert.Same(request1.Method, request2.Method); // reference equality to ensure it was interned. } + protected override MessagePackFormatter CreateFormatter() => new(); + private T Read(object anonymousObject) where T : JsonRpcMessage { @@ -452,23 +380,7 @@ private T Read(object anonymousObject) var writer = new MessagePackWriter(sequence); MessagePackSerializer.Serialize(ref writer, anonymousObject, MessagePackSerializerOptions.Standard); writer.Flush(); - return (T)this.formatter.Deserialize(sequence); - } - - private T Roundtrip(T value) - where T : JsonRpcMessage - { - var sequence = new Sequence(); - this.formatter.Serialize(sequence, value); - var actual = (T)this.formatter.Deserialize(sequence); - return actual; - } - - [DataContract] - public class CustomType - { - [DataMember] - public int Age { get; set; } + return (T)this.Formatter.Deserialize(sequence); } [DataContract] diff --git a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj index 852368f6..cb04a331 100644 --- a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj +++ b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj @@ -23,6 +23,9 @@ + + + diff --git a/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs b/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs new file mode 100644 index 00000000..4250820d --- /dev/null +++ b/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using StreamJsonRpc; +using Xunit.Abstractions; + +public class SystemTextJsonFormatterTests : FormatterTestBase +{ + public SystemTextJsonFormatterTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override SystemTextJsonFormatter CreateFormatter() => new(); +} From e55d170017604887c625f5f55b9ed9db30d13bfe Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 20 Apr 2023 16:12:21 -0600 Subject: [PATCH 16/35] Add top-level property support --- src/StreamJsonRpc/JsonMessageFormatter.cs | 6 +- src/StreamJsonRpc/MessagePackFormatter.cs | 6 +- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 161 ++++++++++++++++++- 3 files changed, 159 insertions(+), 14 deletions(-) diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index 1e01626e..ca68e52e 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -843,7 +843,7 @@ public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T va Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); value = default; - return this.TopLevelPropertyBag is not null && this.TopLevelPropertyBag.TryGetTopLevelProperty(name, out value); + return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; } public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) @@ -898,7 +898,7 @@ public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T va Requires.Argument(!Constants.Result.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); value = default; - return this.TopLevelPropertyBag is not null && this.TopLevelPropertyBag.TryGetTopLevelProperty(name, out value); + return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; } public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) @@ -931,7 +931,7 @@ public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T va Requires.Argument(!Constants.Error.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); value = default; - return this.TopLevelPropertyBag is not null && this.TopLevelPropertyBag.TryGetTopLevelProperty(name, out value); + return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; } public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) diff --git a/src/StreamJsonRpc/MessagePackFormatter.cs b/src/StreamJsonRpc/MessagePackFormatter.cs index c2507268..f9975d25 100644 --- a/src/StreamJsonRpc/MessagePackFormatter.cs +++ b/src/StreamJsonRpc/MessagePackFormatter.cs @@ -2304,7 +2304,7 @@ public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T va Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); value = default; - return this.TopLevelPropertyBag is not null && this.TopLevelPropertyBag.TryGetTopLevelProperty(name, out value); + return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; } public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) @@ -2362,7 +2362,7 @@ public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T va Requires.Argument(!Constants.Result.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); value = default; - return this.TopLevelPropertyBag is not null && this.TopLevelPropertyBag.TryGetTopLevelProperty(name, out value); + return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; } public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) @@ -2425,7 +2425,7 @@ public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T va Requires.Argument(!Constants.Error.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); value = default; - return this.TopLevelPropertyBag is not null && this.TopLevelPropertyBag.TryGetTopLevelProperty(name, out value); + return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; } public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 32296fd2..8598a2ea 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -18,7 +18,7 @@ namespace StreamJsonRpc; /// /// A formatter that emits UTF-8 encoded JSON where user data should be serializable via the . /// -public partial class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcFormatterState +public partial class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcFormatterState, IJsonRpcMessageFactory { private static readonly JsonWriterOptions WriterOptions = new() { }; @@ -57,6 +57,11 @@ public SystemTextJsonFormatter() }); } + private interface IMessageWithTopLevelPropertyBag + { + TopLevelPropertyBag? TopLevelPropertyBag { get; set; } + } + /// public Encoding Encoding { @@ -111,7 +116,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding } else if (document.RootElement.TryGetProperty(Utf8Strings.error, out JsonElement errorElement)) { - JsonRpcError error = new() + JsonRpcError error = new(this) { RequestId = ReadRequestId(), Error = new JsonRpcError.ErrorDetail(this) @@ -139,6 +144,11 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding message.Version = "1.0"; } + if (message is IMessageWithTopLevelPropertyBag messageWithTopLevelPropertyBag) + { + messageWithTopLevelPropertyBag.TopLevelPropertyBag = new(document, this.massagedUserDataSerializerOptions); + } + RequestId ReadRequestId() { return document.RootElement.TryGetProperty(Utf8Strings.id, out JsonElement idElement) @@ -171,22 +181,26 @@ public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message) WriteId(request.RequestId); writer.WriteString(Utf8Strings.method, request.Method); WriteArguments(request); - writer.WriteEndObject(); break; case Protocol.JsonRpcResult result: WriteId(result.RequestId); WriteResult(result); - writer.WriteEndObject(); break; case Protocol.JsonRpcError error: WriteId(error.RequestId); WriteError(error); - writer.WriteEndObject(); break; default: throw new ArgumentException("Unknown message type: " + message.GetType().Name, nameof(message)); } + if (message is IMessageWithTopLevelPropertyBag { TopLevelPropertyBag: TopLevelPropertyBag propertyBag }) + { + propertyBag.WriteProperties(writer); + } + + writer.WriteEndObject(); + void WriteVersion() { switch (message.Version) @@ -275,6 +289,12 @@ void WriteUserData(object? value) } } + Protocol.JsonRpcRequest IJsonRpcMessageFactory.CreateRequestMessage() => new JsonRpcRequest(this); + + Protocol.JsonRpcError IJsonRpcMessageFactory.CreateErrorMessage() => new JsonRpcError(this); + + Protocol.JsonRpcResult IJsonRpcMessageFactory.CreateResultMessage() => new JsonRpcResult(this); + private JsonSerializerOptions MassageUserDataSerializerOptions(JsonSerializerOptions options) { // This is required for $/cancelRequest messages. @@ -311,7 +331,62 @@ private static class Utf8Strings #pragma warning restore SA1300 // Element should begin with upper-case letter } - private class JsonRpcRequest : Protocol.JsonRpcRequest, IJsonRpcMessageBufferManager + private class TopLevelPropertyBag + { + private readonly JsonDocument? incomingMessage; + private readonly JsonSerializerOptions jsonSerializerOptions; + private readonly Dictionary outboundProperties = new(StringComparer.Ordinal); + + /// + /// Initializes a new instance of the class + /// for use with an incoming message. + /// + /// The incoming message. + /// The serializer options to use. + internal TopLevelPropertyBag(JsonDocument incomingMessage, JsonSerializerOptions jsonSerializerOptions) + { + this.incomingMessage = incomingMessage; + this.jsonSerializerOptions = jsonSerializerOptions; + } + + /// + /// Initializes a new instance of the class + /// for use with an outcoming message. + /// + /// The serializer options to use. + internal TopLevelPropertyBag(JsonSerializerOptions jsonSerializerOptions) + { + this.jsonSerializerOptions = jsonSerializerOptions; + } + + internal bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + { + if (this.incomingMessage?.RootElement.TryGetProperty(name, out JsonElement serializedValue) is true) + { + value = serializedValue.Deserialize(this.jsonSerializerOptions); + return true; + } + + value = default; + return false; + } + + internal void SetTopLevelProperty(string name, [MaybeNull] T value) + { + this.outboundProperties[name] = value; + } + + internal void WriteProperties(Utf8JsonWriter writer) + { + foreach (KeyValuePair property in this.outboundProperties) + { + writer.WritePropertyName(property.Key); + JsonSerializer.Serialize(writer, property.Value, this.jsonSerializerOptions); + } + } + } + + private class JsonRpcRequest : Protocol.JsonRpcRequest, IJsonRpcMessageBufferManager, IMessageWithTopLevelPropertyBag { private readonly SystemTextJsonFormatter formatter; @@ -326,6 +401,8 @@ internal JsonRpcRequest(SystemTextJsonFormatter formatter) public override int ArgumentCount => this.argumentCount; + public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } + internal JsonRpcMethodAttribute? ApplicableMethodAttribute { get; private set; } internal JsonElement? JsonArguments @@ -414,6 +491,25 @@ public override bool TryGetArgumentByNameOrIndex(string? name, int position, Typ return valueElement.HasValue; } + public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) + { + Requires.NotNullOrEmpty(name, nameof(name)); + Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); + + this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); + this.TopLevelPropertyBag.SetTopLevelProperty(name, value); + return true; + } + + public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + { + Requires.NotNullOrEmpty(name, nameof(name)); + Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); + + value = default; + return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; + } + private static int CountArguments(JsonElement arguments) { int count = 0; @@ -441,7 +537,7 @@ private static int CountArguments(JsonElement arguments) } } - private class JsonRpcResult : Protocol.JsonRpcResult, IJsonRpcMessageBufferManager + private class JsonRpcResult : Protocol.JsonRpcResult, IJsonRpcMessageBufferManager, IMessageWithTopLevelPropertyBag { private readonly SystemTextJsonFormatter formatter; @@ -452,6 +548,8 @@ internal JsonRpcResult(SystemTextJsonFormatter formatter) this.formatter = formatter; } + public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } + internal JsonElement? JsonResult { get; set; } public void DeserializationComplete(JsonRpcMessage message) @@ -474,6 +572,25 @@ public override T GetResult() : this.JsonResult.Value.Deserialize(this.formatter.massagedUserDataSerializerOptions)!; } + public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) + { + Requires.NotNullOrEmpty(name, nameof(name)); + Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); + + this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); + this.TopLevelPropertyBag.SetTopLevelProperty(name, value); + return true; + } + + public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + { + Requires.NotNullOrEmpty(name, nameof(name)); + Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); + + value = default; + return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; + } + protected internal override void SetExpectedResultType(Type resultType) { Verify.Operation(this.JsonResult is not null, "Result is no longer available or has already been deserialized."); @@ -491,8 +608,17 @@ protected internal override void SetExpectedResultType(Type resultType) } } - private class JsonRpcError : Protocol.JsonRpcError, IJsonRpcMessageBufferManager + private class JsonRpcError : Protocol.JsonRpcError, IJsonRpcMessageBufferManager, IMessageWithTopLevelPropertyBag { + private readonly SystemTextJsonFormatter formatter; + + public JsonRpcError(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + + public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } + internal new ErrorDetail? Error { get => (ErrorDetail?)base.Error; @@ -510,6 +636,25 @@ public void DeserializationComplete(JsonRpcMessage message) } } + public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) + { + Requires.NotNullOrEmpty(name, nameof(name)); + Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); + + this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); + this.TopLevelPropertyBag.SetTopLevelProperty(name, value); + return true; + } + + public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + { + Requires.NotNullOrEmpty(name, nameof(name)); + Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); + + value = default; + return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; + } + internal new class ErrorDetail : Protocol.JsonRpcError.ErrorDetail { private readonly SystemTextJsonFormatter formatter; From 1611b4151a13a70926ff8417d88c411a94b7f7bb Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 21 Apr 2023 10:46:40 -0600 Subject: [PATCH 17/35] Share top-level property code across formatters --- src/StreamJsonRpc/FormatterBase.cs | 151 +++++++++++++++ src/StreamJsonRpc/JsonMessageFormatter.cs | 152 +++++---------- src/StreamJsonRpc/MessagePackFormatter.cs | 181 ++++-------------- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 142 ++++---------- .../HeaderDelimitedMessageHandlerTests.cs | 5 + 5 files changed, 275 insertions(+), 356 deletions(-) diff --git a/src/StreamJsonRpc/FormatterBase.cs b/src/StreamJsonRpc/FormatterBase.cs index 27cccb39..510b5696 100644 --- a/src/StreamJsonRpc/FormatterBase.cs +++ b/src/StreamJsonRpc/FormatterBase.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using System.Reflection; +using System.Runtime.Serialization; using Nerdbank.Streams; using StreamJsonRpc.Protocol; using StreamJsonRpc.Reflection; @@ -50,6 +52,11 @@ public FormatterBase() { } + protected interface IMessageWithTopLevelPropertyBag + { + TopLevelPropertyBagBase? TopLevelPropertyBag { get; set; } + } + /// public RequestId SerializingMessageWithId { get; private set; } @@ -293,4 +300,148 @@ public void Dispose() this.formatter.SerializingRequest = false; } } + + protected abstract class TopLevelPropertyBagBase + { + private readonly bool isOutbound; + private Dictionary? outboundProperties; + + public TopLevelPropertyBagBase(bool isOutbound) + { + this.isOutbound = isOutbound; + } + + protected Dictionary OutboundProperties + { + get + { + if (!this.isOutbound) + { + Verify.FailOperation(Resources.OutboundMessageOnly); + } + + return this.outboundProperties ??= new Dictionary(StringComparer.Ordinal); + } + } + + internal static void ValidatePropertyName(string name) + { + Requires.NotNullOrEmpty(name, nameof(name)); + Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); + } + + internal void SetTopLevelProperty(string name, [MaybeNull] T value) + { + if (this.OutboundProperties is null) + { + throw new InvalidOperationException(Resources.OutboundMessageOnly); + } + + this.OutboundProperties[name] = (typeof(T), value); + } + + protected internal abstract bool TryGetTopLevelProperty(string name, [MaybeNull] out T value); + } + + protected abstract class JsonRpcRequestBase : JsonRpcRequest, IJsonRpcMessageBufferManager, IMessageWithTopLevelPropertyBag + { + [Newtonsoft.Json.JsonIgnore] + [IgnoreDataMember] + public TopLevelPropertyBagBase? TopLevelPropertyBag { get; set; } + + void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message) + { + Assumes.True(message == this); + this.ReleaseBuffers(); + } + + public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) + { + TopLevelPropertyBagBase.ValidatePropertyName(name); + this.TopLevelPropertyBag ??= this.CreateTopLevelPropertyBag(); + this.TopLevelPropertyBag?.SetTopLevelProperty(name, value); + return this.TopLevelPropertyBag is not null; + } + + public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + { + TopLevelPropertyBagBase.ValidatePropertyName(name); + value = default; + return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; + } + + protected abstract TopLevelPropertyBagBase? CreateTopLevelPropertyBag(); + + protected virtual void ReleaseBuffers() + { + } + } + + protected abstract class JsonRpcErrorBase : JsonRpcError, IJsonRpcMessageBufferManager, IMessageWithTopLevelPropertyBag + { + [Newtonsoft.Json.JsonIgnore] + [IgnoreDataMember] + public TopLevelPropertyBagBase? TopLevelPropertyBag { get; set; } + + void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message) + { + Assumes.True(message == this); + this.ReleaseBuffers(); + } + + public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) + { + TopLevelPropertyBagBase.ValidatePropertyName(name); + this.TopLevelPropertyBag ??= this.CreateTopLevelPropertyBag(); + this.TopLevelPropertyBag?.SetTopLevelProperty(name, value); + return this.TopLevelPropertyBag is not null; + } + + public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + { + TopLevelPropertyBagBase.ValidatePropertyName(name); + value = default; + return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; + } + + protected abstract TopLevelPropertyBagBase? CreateTopLevelPropertyBag(); + + protected virtual void ReleaseBuffers() + { + } + } + + protected abstract class JsonRpcResultBase : JsonRpcResult, IJsonRpcMessageBufferManager, IMessageWithTopLevelPropertyBag + { + [Newtonsoft.Json.JsonIgnore] + [IgnoreDataMember] + public TopLevelPropertyBagBase? TopLevelPropertyBag { get; set; } + + void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message) + { + Assumes.True(message == this); + this.ReleaseBuffers(); + } + + public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) + { + TopLevelPropertyBagBase.ValidatePropertyName(name); + this.TopLevelPropertyBag ??= this.CreateTopLevelPropertyBag(); + this.TopLevelPropertyBag?.SetTopLevelProperty(name, value); + return this.TopLevelPropertyBag is not null; + } + + public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + { + TopLevelPropertyBagBase.ValidatePropertyName(name); + value = default; + return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; + } + + protected abstract TopLevelPropertyBagBase? CreateTopLevelPropertyBag(); + + protected virtual void ReleaseBuffers() + { + } + } } diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index ca68e52e..f18b0661 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -150,11 +150,6 @@ public JsonMessageFormatter(Encoding encoding) }; } - private interface IMessageWithTopLevelPropertyBag - { - TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - } - /// /// Gets or sets the encoding to use for transmitted messages. /// @@ -328,15 +323,9 @@ public JToken Serialize(JsonRpcMessage message) } // Copy over extra top-level properties. - if (message is IMessageWithTopLevelPropertyBag { TopLevelPropertyBag: { } bag }) + if (message is IMessageWithTopLevelPropertyBag { TopLevelPropertyBag: TopLevelPropertyBag bag }) { - foreach (JProperty property in bag.Properties) - { - if (json[property.Name] is null) - { - json[property.Name] = property.Value; - } - } + bag.WriteProperties(json); } return json; @@ -678,14 +667,14 @@ private RequestId NormalizeId(RequestId id) return id; } - private class TopLevelPropertyBag + private class TopLevelPropertyBag : TopLevelPropertyBagBase { private readonly JsonSerializer jsonSerializer; /// - /// The incoming message or the envelope used to store the top-level properties to add to the outbound message. + /// The incoming message. /// - private JObject envelope; + private JObject? incomingEnvelope; /// /// Initializes a new instance of the class @@ -694,9 +683,10 @@ private class TopLevelPropertyBag /// The serializer to use. /// The incoming message. internal TopLevelPropertyBag(JsonSerializer jsonSerializer, JObject incomingMessage) + : base(isOutbound: false) { this.jsonSerializer = jsonSerializer; - this.envelope = incomingMessage; + this.incomingEnvelope = incomingMessage; } /// @@ -705,16 +695,40 @@ internal TopLevelPropertyBag(JsonSerializer jsonSerializer, JObject incomingMess /// /// The serializer to use. internal TopLevelPropertyBag(JsonSerializer jsonSerializer) + : base(isOutbound: true) { this.jsonSerializer = jsonSerializer; - this.envelope = new JObject(); } - internal IEnumerable Properties => this.envelope?.Properties() ?? throw new InvalidOperationException(Resources.OutboundMessageOnly); + internal void WriteProperties(JToken envelope) + { + if (this.incomingEnvelope is not null) + { + // We're actually re-transmitting an incoming message (remote target feature). + // We need to copy all the properties that were in the original message. + foreach (JProperty property in this.incomingEnvelope.Properties()) + { + if (!Constants.Request.IsPropertyReserved(property.Name) && envelope[property.Name] is null) + { + envelope[property.Name] = property.Value; + } + } + } + else + { + foreach (KeyValuePair property in this.OutboundProperties) + { + if (envelope[property.Key] is null) + { + envelope[property.Key] = property.Value.Item2 is null ? JValue.CreateNull() : JToken.FromObject(property.Value.Item2, this.jsonSerializer); + } + } + } + } - internal bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + protected internal override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) { - if (this.envelope.TryGetValue(name, out JToken? serializedValue) is true) + if (this.incomingEnvelope!.TryGetValue(name, out JToken? serializedValue) is true) { value = serializedValue is null ? default : serializedValue.ToObject(this.jsonSerializer); return true; @@ -723,16 +737,11 @@ internal bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) value = default; return false; } - - internal void SetTopLevelProperty(string name, [MaybeNull] T value) - { - this.envelope[name] = value is null ? null : JToken.FromObject(value, this.jsonSerializer); - } } [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [DataContract] - private class OutboundJsonRpcRequest : Protocol.JsonRpcRequest, IMessageWithTopLevelPropertyBag + private class OutboundJsonRpcRequest : JsonRpcRequestBase { private readonly JsonMessageFormatter formatter; @@ -741,24 +750,12 @@ internal OutboundJsonRpcRequest(JsonMessageFormatter formatter) this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); } - [JsonIgnore] - [IgnoreDataMember] - public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - - public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.formatter.JsonSerializer); - this.TopLevelPropertyBag.SetTopLevelProperty(name, value); - return true; - } + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.JsonSerializer); } [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [DataContract] - private class JsonRpcRequest : Protocol.JsonRpcRequest, IMessageWithTopLevelPropertyBag + private class JsonRpcRequest : JsonRpcRequestBase { private readonly JsonMessageFormatter formatter; @@ -767,10 +764,6 @@ internal JsonRpcRequest(JsonMessageFormatter formatter) this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); } - [JsonIgnore] - [IgnoreDataMember] - public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan parameters, Span typedArguments) { using (this.formatter.TrackDeserialization(this, parameters)) @@ -837,29 +830,12 @@ public override bool TryGetArgumentByNameOrIndex(string? name, int position, Typ return false; } - public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - value = default; - return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; - } - - public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.formatter.JsonSerializer); - this.TopLevelPropertyBag.SetTopLevelProperty(name, value); - return true; - } + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.JsonSerializer); } [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [DataContract] - private class JsonRpcResult : Protocol.JsonRpcResult, IMessageWithTopLevelPropertyBag + private class JsonRpcResult : JsonRpcResultBase { private readonly JsonSerializer jsonSerializer; @@ -868,10 +844,6 @@ internal JsonRpcResult(JsonSerializer jsonSerializer) this.jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); } - [JsonIgnore] - [IgnoreDataMember] - public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - public override T GetResult() { Verify.Operation(this.Result is not null, "This instance hasn't been initialized with a result yet."); @@ -892,27 +864,10 @@ public override T GetResult() } } - public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Result.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - value = default; - return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; - } - - public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Result.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.jsonSerializer); - this.TopLevelPropertyBag.SetTopLevelProperty(name, value); - return true; - } + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.jsonSerializer); } - private class JsonRpcError : Protocol.JsonRpcError, IMessageWithTopLevelPropertyBag + private class JsonRpcError : JsonRpcErrorBase { private readonly JsonSerializer jsonSerializer; @@ -921,28 +876,7 @@ internal JsonRpcError(JsonSerializer jsonSerializer) this.jsonSerializer = Requires.NotNull(jsonSerializer, nameof(jsonSerializer)); } - [JsonIgnore] - [IgnoreDataMember] - public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - - public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Error.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - value = default; - return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; - } - - public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Error.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.jsonSerializer); - this.TopLevelPropertyBag.SetTopLevelProperty(name, value); - return true; - } + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.jsonSerializer); } [DataContract] diff --git a/src/StreamJsonRpc/MessagePackFormatter.cs b/src/StreamJsonRpc/MessagePackFormatter.cs index f9975d25..70c45b31 100644 --- a/src/StreamJsonRpc/MessagePackFormatter.cs +++ b/src/StreamJsonRpc/MessagePackFormatter.cs @@ -147,11 +147,6 @@ private interface IJsonRpcMessagePackRetention ReadOnlySequence OriginalMessagePack { get; } } - private interface IMessageWithTopLevelPropertyBag - { - TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - } - /// /// Gets the default used for user data (arguments, return values and errors) in RPC calls /// prior to any call to . @@ -1652,7 +1647,7 @@ public Protocol.JsonRpcRequest Deserialize(ref MessagePackReader reader, Message public void Serialize(ref MessagePackWriter writer, Protocol.JsonRpcRequest value, MessagePackSerializerOptions options) { - var topLevelPropertyBagMessage = value as IMessageWithTopLevelPropertyBag; + var topLevelPropertyBag = (TopLevelPropertyBag?)(value as IMessageWithTopLevelPropertyBag)?.TopLevelPropertyBag; int mapElementCount = value.RequestId.IsEmpty ? 3 : 4; if (value.TraceParent?.Length > 0) @@ -1664,7 +1659,7 @@ public void Serialize(ref MessagePackWriter writer, Protocol.JsonRpcRequest valu } } - mapElementCount += topLevelPropertyBagMessage?.TopLevelPropertyBag?.PropertyCount ?? 0; + mapElementCount += topLevelPropertyBag?.PropertyCount ?? 0; writer.WriteMapHeader(mapElementCount); WriteProtocolVersionPropertyAndValue(ref writer, value.Version); @@ -1728,7 +1723,7 @@ public void Serialize(ref MessagePackWriter writer, Protocol.JsonRpcRequest valu } } - topLevelPropertyBagMessage?.TopLevelPropertyBag?.WritePropertiesAndClear(ref writer); + topLevelPropertyBag?.WriteProperties(ref writer); } private static void WriteTraceState(ref MessagePackWriter writer, string traceState) @@ -1851,7 +1846,7 @@ public void Serialize(ref MessagePackWriter writer, Protocol.JsonRpcResult value var topLevelPropertyBagMessage = value as IMessageWithTopLevelPropertyBag; int mapElementCount = 3; - mapElementCount += topLevelPropertyBagMessage?.TopLevelPropertyBag?.PropertyCount ?? 0; + mapElementCount += (topLevelPropertyBagMessage?.TopLevelPropertyBag as TopLevelPropertyBag)?.PropertyCount ?? 0; writer.WriteMapHeader(mapElementCount); WriteProtocolVersionPropertyAndValue(ref writer, value.Version); @@ -1869,7 +1864,7 @@ public void Serialize(ref MessagePackWriter writer, Protocol.JsonRpcResult value DynamicObjectTypeFallbackFormatter.Instance.Serialize(ref writer, value.Result, this.formatter.userDataSerializationOptions); } - topLevelPropertyBagMessage?.TopLevelPropertyBag?.WritePropertiesAndClear(ref writer); + (topLevelPropertyBagMessage?.TopLevelPropertyBag as TopLevelPropertyBag)?.WriteProperties(ref writer); } } @@ -1925,10 +1920,10 @@ public Protocol.JsonRpcError Deserialize(ref MessagePackReader reader, MessagePa public void Serialize(ref MessagePackWriter writer, Protocol.JsonRpcError value, MessagePackSerializerOptions options) { - var topLevelPropertyBagMessage = value as IMessageWithTopLevelPropertyBag; + var topLevelPropertyBag = (TopLevelPropertyBag?)(value as IMessageWithTopLevelPropertyBag)?.TopLevelPropertyBag; int mapElementCount = 3; - mapElementCount += topLevelPropertyBagMessage?.TopLevelPropertyBag?.PropertyCount ?? 0; + mapElementCount += topLevelPropertyBag?.PropertyCount ?? 0; writer.WriteMapHeader(mapElementCount); WriteProtocolVersionPropertyAndValue(ref writer, value.Version); @@ -1939,7 +1934,7 @@ public void Serialize(ref MessagePackWriter writer, Protocol.JsonRpcError value, ErrorPropertyName.Write(ref writer); options.Resolver.GetFormatterWithVerify().Serialize(ref writer, value.Error, options); - topLevelPropertyBagMessage?.TopLevelPropertyBag?.WritePropertiesAndClear(ref writer); + topLevelPropertyBag?.WriteProperties(ref writer); } } @@ -2079,12 +2074,10 @@ public unsafe void Serialize(ref MessagePackWriter writer, TraceParent value, Me } } - private class TopLevelPropertyBag + private class TopLevelPropertyBag : TopLevelPropertyBagBase { private readonly MessagePackSerializerOptions serializerOptions; private readonly IReadOnlyDictionary>? inboundUnknownProperties; - private Dictionary>? outboundUnknownProperties; - private bool outboundPropertiesAlreadyWritten; /// /// Initializes a new instance of the class @@ -2093,6 +2086,7 @@ private class TopLevelPropertyBag /// The serializer options to use for this data. /// The map of unrecognized inbound properties. internal TopLevelPropertyBag(MessagePackSerializerOptions userDataSerializationOptions, IReadOnlyDictionary> inboundUnknownProperties) + : base(isOutbound: false) { this.serializerOptions = userDataSerializationOptions; this.inboundUnknownProperties = inboundUnknownProperties; @@ -2104,40 +2098,32 @@ internal TopLevelPropertyBag(MessagePackSerializerOptions userDataSerializationO /// /// The serializer options to use for this data. internal TopLevelPropertyBag(MessagePackSerializerOptions serializerOptions) + : base(isOutbound: true) { this.serializerOptions = serializerOptions; - this.outboundUnknownProperties = new Dictionary>(); } - internal int PropertyCount => this.outboundUnknownProperties?.Count ?? this.inboundUnknownProperties?.Count ?? 0; + internal int PropertyCount => this.OutboundProperties?.Count ?? this.inboundUnknownProperties?.Count ?? 0; /// /// Writes the properties tracked by this collection to a messagepack writer. /// /// The writer to use. - internal void WritePropertiesAndClear(ref MessagePackWriter writer) + internal void WriteProperties(ref MessagePackWriter writer) { - if (this.outboundUnknownProperties is null) + if (this.OutboundProperties is null) { throw new InvalidOperationException(Resources.OutboundMessageOnly); } - Verify.Operation(!this.outboundPropertiesAlreadyWritten, Resources.UsableOnceOnly); - - foreach (KeyValuePair> entry in this.outboundUnknownProperties) + foreach (KeyValuePair entry in this.OutboundProperties) { writer.Write(entry.Key); - writer.WriteRaw(entry.Value); - entry.Value.Reset(); + MessagePackSerializer.Serialize(entry.Value.Item1, ref writer, entry.Value.Item2, this.serializerOptions); } - - this.outboundUnknownProperties.Clear(); - - // Throw if this method is called again, since recycling memory here means the operation cannot be repeated. - this.outboundPropertiesAlreadyWritten = true; } - internal bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + protected internal override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) { if (this.inboundUnknownProperties is null) { @@ -2155,25 +2141,11 @@ internal bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) return false; } - - internal void SetTopLevelProperty(string name, [MaybeNull] T value) - { - if (this.outboundUnknownProperties is null) - { - throw new InvalidOperationException(Resources.OutboundMessageOnly); - } - - Sequence buffer = new(); - MessagePackWriter writer = new(buffer); - MessagePackSerializer.Serialize(ref writer, value, this.serializerOptions); - writer.Flush(); - this.outboundUnknownProperties[name] = buffer; - } } [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [DataContract] - private class OutboundJsonRpcRequest : Protocol.JsonRpcRequest, IMessageWithTopLevelPropertyBag + private class OutboundJsonRpcRequest : JsonRpcRequestBase { private readonly MessagePackFormatter formatter; @@ -2182,22 +2154,12 @@ internal OutboundJsonRpcRequest(MessagePackFormatter formatter) this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); } - public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - - public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.formatter.userDataSerializationOptions); - this.TopLevelPropertyBag.SetTopLevelProperty(name, value); - return true; - } + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.userDataSerializationOptions); } [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [DataContract] - private class JsonRpcRequest : Protocol.JsonRpcRequest, IJsonRpcMessageBufferManager, IJsonRpcMessagePackRetention, IMessageWithTopLevelPropertyBag + private class JsonRpcRequest : JsonRpcRequestBase, IJsonRpcMessagePackRetention { private readonly MessagePackFormatter formatter; @@ -2212,26 +2174,12 @@ internal JsonRpcRequest(MessagePackFormatter formatter) public ReadOnlySequence OriginalMessagePack { get; internal set; } - public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - internal ReadOnlySequence MsgPackArguments { get; set; } internal IReadOnlyDictionary>? MsgPackNamedArguments { get; set; } internal IReadOnlyList>? MsgPackPositionalArguments { get; set; } - void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message) - { - Assumes.True(message == this); - - // Clear references to buffers that we are no longer entitled to. - this.MsgPackNamedArguments = null; - this.MsgPackPositionalArguments = null; - this.TopLevelPropertyBag = null; - this.MsgPackArguments = default; - this.OriginalMessagePack = default; - } - public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan parameters, Span typedArguments) { using (this.formatter.TrackDeserialization(this, parameters)) @@ -2298,29 +2246,22 @@ public override bool TryGetArgumentByNameOrIndex(string? name, int position, Typ } } - public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + protected override void ReleaseBuffers() { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - value = default; - return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; + base.ReleaseBuffers(); + this.MsgPackNamedArguments = null; + this.MsgPackPositionalArguments = null; + this.TopLevelPropertyBag = null; + this.MsgPackArguments = default; + this.OriginalMessagePack = default; } - public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.formatter.userDataSerializationOptions); - this.TopLevelPropertyBag.SetTopLevelProperty(name, value); - return true; - } + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.userDataSerializationOptions); } [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [DataContract] - private class JsonRpcResult : Protocol.JsonRpcResult, IJsonRpcMessageBufferManager, IJsonRpcMessagePackRetention, IMessageWithTopLevelPropertyBag + private class JsonRpcResult : JsonRpcResultBase, IJsonRpcMessagePackRetention { private readonly MessagePackSerializerOptions serializerOptions; @@ -2333,17 +2274,8 @@ internal JsonRpcResult(MessagePackSerializerOptions serializerOptions) public ReadOnlySequence OriginalMessagePack { get; internal set; } - public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - internal ReadOnlySequence MsgPackResult { get; set; } - void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message) - { - Assumes.True(message == this); - this.MsgPackResult = default; - this.OriginalMessagePack = default; - } - public override T GetResult() { if (this.resultDeserializationException is not null) @@ -2356,25 +2288,6 @@ public override T GetResult() : MessagePackSerializer.Deserialize(this.MsgPackResult, this.serializerOptions); } - public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Result.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - value = default; - return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; - } - - public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Result.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.serializerOptions); - this.TopLevelPropertyBag.SetTopLevelProperty(name, value); - return true; - } - protected internal override void SetExpectedResultType(Type resultType) { Verify.Operation(!this.MsgPackResult.IsEmpty, "Result is no longer available or has already been deserialized."); @@ -2391,11 +2304,20 @@ protected internal override void SetExpectedResultType(Type resultType) this.resultDeserializationException = ex; } } + + protected override void ReleaseBuffers() + { + base.ReleaseBuffers(); + this.MsgPackResult = default; + this.OriginalMessagePack = default; + } + + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.serializerOptions); } [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [DataContract] - private class JsonRpcError : Protocol.JsonRpcError, IJsonRpcMessageBufferManager, IJsonRpcMessagePackRetention, IMessageWithTopLevelPropertyBag + private class JsonRpcError : JsonRpcErrorBase, IJsonRpcMessagePackRetention { private readonly MessagePackSerializerOptions serializerOptions; @@ -2406,11 +2328,11 @@ public JsonRpcError(MessagePackSerializerOptions serializerOptions) public ReadOnlySequence OriginalMessagePack { get; internal set; } - public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.serializerOptions); - void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message) + protected override void ReleaseBuffers() { - Assumes.True(message == this); + base.ReleaseBuffers(); if (this.Error is ErrorDetail privateDetail) { privateDetail.MsgPackData = default; @@ -2419,25 +2341,6 @@ void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message this.OriginalMessagePack = default; } - public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Error.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - value = default; - return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; - } - - public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Error.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.serializerOptions); - this.TopLevelPropertyBag.SetTopLevelProperty(name, value); - return true; - } - [DataContract] internal new class ErrorDetail : Protocol.JsonRpcError.ErrorDetail { diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 8598a2ea..fe475ffe 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -57,11 +57,6 @@ public SystemTextJsonFormatter() }); } - private interface IMessageWithTopLevelPropertyBag - { - TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - } - /// public Encoding Encoding { @@ -146,7 +141,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding if (message is IMessageWithTopLevelPropertyBag messageWithTopLevelPropertyBag) { - messageWithTopLevelPropertyBag.TopLevelPropertyBag = new(document, this.massagedUserDataSerializerOptions); + messageWithTopLevelPropertyBag.TopLevelPropertyBag = new TopLevelPropertyBag(document, this.massagedUserDataSerializerOptions); } RequestId ReadRequestId() @@ -331,11 +326,10 @@ private static class Utf8Strings #pragma warning restore SA1300 // Element should begin with upper-case letter } - private class TopLevelPropertyBag + private class TopLevelPropertyBag : TopLevelPropertyBagBase { private readonly JsonDocument? incomingMessage; private readonly JsonSerializerOptions jsonSerializerOptions; - private readonly Dictionary outboundProperties = new(StringComparer.Ordinal); /// /// Initializes a new instance of the class @@ -344,6 +338,7 @@ private class TopLevelPropertyBag /// The incoming message. /// The serializer options to use. internal TopLevelPropertyBag(JsonDocument incomingMessage, JsonSerializerOptions jsonSerializerOptions) + : base(isOutbound: false) { this.incomingMessage = incomingMessage; this.jsonSerializerOptions = jsonSerializerOptions; @@ -355,11 +350,21 @@ internal TopLevelPropertyBag(JsonDocument incomingMessage, JsonSerializerOptions /// /// The serializer options to use. internal TopLevelPropertyBag(JsonSerializerOptions jsonSerializerOptions) + : base(isOutbound: true) { this.jsonSerializerOptions = jsonSerializerOptions; } - internal bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + internal void WriteProperties(Utf8JsonWriter writer) + { + foreach (KeyValuePair property in this.OutboundProperties) + { + writer.WritePropertyName(property.Key); + JsonSerializer.Serialize(writer, property.Value.Item2, this.jsonSerializerOptions); + } + } + + protected internal override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) { if (this.incomingMessage?.RootElement.TryGetProperty(name, out JsonElement serializedValue) is true) { @@ -370,23 +375,9 @@ internal bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) value = default; return false; } - - internal void SetTopLevelProperty(string name, [MaybeNull] T value) - { - this.outboundProperties[name] = value; - } - - internal void WriteProperties(Utf8JsonWriter writer) - { - foreach (KeyValuePair property in this.outboundProperties) - { - writer.WritePropertyName(property.Key); - JsonSerializer.Serialize(writer, property.Value, this.jsonSerializerOptions); - } - } } - private class JsonRpcRequest : Protocol.JsonRpcRequest, IJsonRpcMessageBufferManager, IMessageWithTopLevelPropertyBag + private class JsonRpcRequest : JsonRpcRequestBase { private readonly SystemTextJsonFormatter formatter; @@ -401,10 +392,6 @@ internal JsonRpcRequest(SystemTextJsonFormatter formatter) public override int ArgumentCount => this.argumentCount; - public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - - internal JsonRpcMethodAttribute? ApplicableMethodAttribute { get; private set; } - internal JsonElement? JsonArguments { get => this.jsonArguments; @@ -418,14 +405,6 @@ internal JsonElement? JsonArguments } } - public void DeserializationComplete(JsonRpcMessage message) - { - Assumes.True(message == this); - - // Clear references to buffers that we are no longer entitled to. - this.jsonArguments = null; - } - public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan parameters, Span typedArguments) { using (this.formatter.TrackDeserialization(this, parameters)) @@ -491,23 +470,12 @@ public override bool TryGetArgumentByNameOrIndex(string? name, int position, Typ return valueElement.HasValue; } - public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); - this.TopLevelPropertyBag.SetTopLevelProperty(name, value); - return true; - } + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); - public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + protected override void ReleaseBuffers() { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - value = default; - return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; + base.ReleaseBuffers(); + this.jsonArguments = null; } private static int CountArguments(JsonElement arguments) @@ -537,7 +505,7 @@ private static int CountArguments(JsonElement arguments) } } - private class JsonRpcResult : Protocol.JsonRpcResult, IJsonRpcMessageBufferManager, IMessageWithTopLevelPropertyBag + private class JsonRpcResult : JsonRpcResultBase { private readonly SystemTextJsonFormatter formatter; @@ -548,18 +516,8 @@ internal JsonRpcResult(SystemTextJsonFormatter formatter) this.formatter = formatter; } - public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - internal JsonElement? JsonResult { get; set; } - public void DeserializationComplete(JsonRpcMessage message) - { - Assumes.True(message == this); - - // Clear references to buffers that we are no longer entitled to. - this.JsonResult = null; - } - public override T GetResult() { if (this.resultDeserializationException is not null) @@ -572,25 +530,6 @@ public override T GetResult() : this.JsonResult.Value.Deserialize(this.formatter.massagedUserDataSerializerOptions)!; } - public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); - this.TopLevelPropertyBag.SetTopLevelProperty(name, value); - return true; - } - - public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - value = default; - return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; - } - protected internal override void SetExpectedResultType(Type resultType) { Verify.Operation(this.JsonResult is not null, "Result is no longer available or has already been deserialized."); @@ -606,9 +545,17 @@ protected internal override void SetExpectedResultType(Type resultType) this.resultDeserializationException = ex; } } + + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); + + protected override void ReleaseBuffers() + { + base.ReleaseBuffers(); + this.JsonResult = null; + } } - private class JsonRpcError : Protocol.JsonRpcError, IJsonRpcMessageBufferManager, IMessageWithTopLevelPropertyBag + private class JsonRpcError : JsonRpcErrorBase { private readonly SystemTextJsonFormatter formatter; @@ -617,44 +564,23 @@ public JsonRpcError(SystemTextJsonFormatter formatter) this.formatter = formatter; } - public TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - internal new ErrorDetail? Error { get => (ErrorDetail?)base.Error; set => base.Error = value; } - public void DeserializationComplete(JsonRpcMessage message) - { - Assumes.True(message == this); + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); - // Clear references to buffers that we are no longer entitled to. + protected override void ReleaseBuffers() + { + base.ReleaseBuffers(); if (this.Error is { } detail) { detail.JsonData = null; } } - public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - this.TopLevelPropertyBag ??= new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); - this.TopLevelPropertyBag.SetTopLevelProperty(name, value); - return true; - } - - public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - Requires.Argument(!Constants.Request.IsPropertyReserved(name), nameof(name), Resources.ReservedPropertyName); - - value = default; - return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; - } - internal new class ErrorDetail : Protocol.JsonRpcError.ErrorDetail { private readonly SystemTextJsonFormatter formatter; @@ -777,7 +703,7 @@ public Converter(SystemTextJsonFormatter formatter) _ => throw new NotSupportedException("Unsupported token type."), // Ideally, we should *copy* the token so we can retain it and replay it later. }; - bool clientRequiresNamedArgs = this.formatter.DeserializingMessage is JsonRpcRequest { ApplicableMethodAttribute: { ClientRequiresNamedArguments: true } }; + bool clientRequiresNamedArgs = this.formatter.ApplicableMethodAttributeOnDeserializingMethod is { ClientRequiresNamedArguments: true }; return (IProgress?)this.formatter.FormatterProgressTracker.CreateProgress(this.formatter.JsonRpc, token, typeToConvert, clientRequiresNamedArgs); } diff --git a/test/StreamJsonRpc.Tests/HeaderDelimitedMessageHandlerTests.cs b/test/StreamJsonRpc.Tests/HeaderDelimitedMessageHandlerTests.cs index 9b1c12ca..17cd363d 100644 --- a/test/StreamJsonRpc.Tests/HeaderDelimitedMessageHandlerTests.cs +++ b/test/StreamJsonRpc.Tests/HeaderDelimitedMessageHandlerTests.cs @@ -6,6 +6,7 @@ using System.Text; using StreamJsonRpc; using StreamJsonRpc.Protocol; +using StreamJsonRpc.Reflection; using Xunit; using Xunit.Abstractions; @@ -63,6 +64,7 @@ public async Task ReadCoreAsync_HandlesSpacingCorrectly() var readContent = (JsonRpcRequest?)await this.handler.ReadAsync(CancellationToken.None); Assert.Equal("test", readContent!.Method); + (this.handler as IJsonRpcMessageBufferManager)?.DeserializationComplete(readContent); this.receivingStream.Position = 0; this.receivingStream.SetLength(0); @@ -78,6 +80,7 @@ public async Task ReadCoreAsync_HandlesSpacingCorrectly() readContent = (JsonRpcRequest?)await this.handler.ReadAsync(CancellationToken.None); Assert.Equal("test", readContent!.Method); + (this.handler as IJsonRpcMessageBufferManager)?.DeserializationComplete(readContent); } [Fact] @@ -96,6 +99,7 @@ public async Task ReadCoreAsync_HandlesUtf8CharsetCorrectly() var readContent = (JsonRpcRequest?)await this.handler.ReadAsync(CancellationToken.None); Assert.Equal("test", readContent!.Method); + (this.handler as IJsonRpcMessageBufferManager)?.DeserializationComplete(readContent); this.receivingStream.Position = 0; this.receivingStream.SetLength(0); @@ -113,6 +117,7 @@ public async Task ReadCoreAsync_HandlesUtf8CharsetCorrectly() readContent = (JsonRpcRequest?)await this.handler.ReadAsync(CancellationToken.None); Assert.Equal("test", readContent!.Method); + (this.handler as IJsonRpcMessageBufferManager)?.DeserializationComplete(readContent); } [Fact] From 30619e9d605b979973ef6d1054f958203c7475ca Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 21 Apr 2023 12:27:43 -0600 Subject: [PATCH 18/35] Add callout for top-level and remote target support of alternative formatters --- src/StreamJsonRpc/FormatterBase.cs | 61 ++++++++++++++++++- src/StreamJsonRpc/JsonMessageFormatter.cs | 4 +- src/StreamJsonRpc/MessagePackFormatter.cs | 26 +++++--- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 17 +++++- .../netstandard2.0/PublicAPI.Unshipped.txt | 33 +++++++++- .../netstandard2.1/PublicAPI.Unshipped.txt | 33 +++++++++- ...pcRemoteTargetJsonMessageFormatterTests.cs | 14 +++++ ...pcRemoteTargetMessagePackFormatterTests.cs | 19 ++++++ ...emoteTargetSystemTextJsonFormatterTests.cs | 18 ++++++ .../JsonRpcRemoteTargetTests.cs | 12 ++-- .../StreamJsonRpc.Tests.csproj | 3 + test/StreamJsonRpc.Tests/Usings.cs | 4 ++ 12 files changed, 225 insertions(+), 19 deletions(-) create mode 100644 test/StreamJsonRpc.Tests/JsonRpcRemoteTargetJsonMessageFormatterTests.cs create mode 100644 test/StreamJsonRpc.Tests/JsonRpcRemoteTargetMessagePackFormatterTests.cs create mode 100644 test/StreamJsonRpc.Tests/JsonRpcRemoteTargetSystemTextJsonFormatterTests.cs create mode 100644 test/StreamJsonRpc.Tests/Usings.cs diff --git a/src/StreamJsonRpc/FormatterBase.cs b/src/StreamJsonRpc/FormatterBase.cs index 510b5696..ddc6498b 100644 --- a/src/StreamJsonRpc/FormatterBase.cs +++ b/src/StreamJsonRpc/FormatterBase.cs @@ -52,8 +52,14 @@ public FormatterBase() { } + /// + /// An interface implemented by all the -derived nested types (, , ) to allow them to carry arbitrary top-level properties on behalf of the application. + /// protected interface IMessageWithTopLevelPropertyBag { + /// + /// Gets or sets the top-level property bag for this message. + /// TopLevelPropertyBagBase? TopLevelPropertyBag { get; set; } } @@ -301,17 +307,28 @@ public void Dispose() } } + /// + /// A base class for top-level property bags that should be declared in the derived formatter class. + /// protected abstract class TopLevelPropertyBagBase { private readonly bool isOutbound; private Dictionary? outboundProperties; + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether this bag belongs to an outbound message. public TopLevelPropertyBagBase(bool isOutbound) { this.isOutbound = isOutbound; } - protected Dictionary OutboundProperties + /// + /// Gets a dictionary of top-level properties that should be serialized. + /// + /// Thrown if called on an inbound message. + protected Dictionary OutboundProperties { get { @@ -340,11 +357,25 @@ internal void SetTopLevelProperty(string name, [MaybeNull] T value) this.OutboundProperties[name] = (typeof(T), value); } + /// + /// Deserializes the value of a top-level property. + /// + /// The type of object expected by the caller. + /// The name of the top-level property. + /// Receives the value of the property. + /// A value indicating whether the property exists. + /// A formatter-specific exception may be thrown if the property exists but the value cannot be deserialized to a . protected internal abstract bool TryGetTopLevelProperty(string name, [MaybeNull] out T value); } + /// + /// A base class for formatter-specific implementations. + /// protected abstract class JsonRpcRequestBase : JsonRpcRequest, IJsonRpcMessageBufferManager, IMessageWithTopLevelPropertyBag { + /// + /// Gets or sets the top-level property bag for this message. + /// [Newtonsoft.Json.JsonIgnore] [IgnoreDataMember] public TopLevelPropertyBagBase? TopLevelPropertyBag { get; set; } @@ -355,6 +386,7 @@ void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message this.ReleaseBuffers(); } + /// public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) { TopLevelPropertyBagBase.ValidatePropertyName(name); @@ -363,6 +395,7 @@ public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) return this.TopLevelPropertyBag is not null; } + /// public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) { TopLevelPropertyBagBase.ValidatePropertyName(name); @@ -370,15 +403,27 @@ public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T va return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; } + /// + /// Creates a new instance of the top-level property bag for this message. + /// protected abstract TopLevelPropertyBagBase? CreateTopLevelPropertyBag(); + /// + /// When overridden by derived types, clears references to all buffers that may have been used for deserialization. + /// protected virtual void ReleaseBuffers() { } } + /// + /// A base class for formatter-specific implementations. + /// protected abstract class JsonRpcErrorBase : JsonRpcError, IJsonRpcMessageBufferManager, IMessageWithTopLevelPropertyBag { + /// + /// Gets or sets the top-level property bag for this message. + /// [Newtonsoft.Json.JsonIgnore] [IgnoreDataMember] public TopLevelPropertyBagBase? TopLevelPropertyBag { get; set; } @@ -389,6 +434,7 @@ void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message this.ReleaseBuffers(); } + /// public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) { TopLevelPropertyBagBase.ValidatePropertyName(name); @@ -397,6 +443,7 @@ public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) return this.TopLevelPropertyBag is not null; } + /// public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) { TopLevelPropertyBagBase.ValidatePropertyName(name); @@ -404,15 +451,23 @@ public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T va return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; } + /// protected abstract TopLevelPropertyBagBase? CreateTopLevelPropertyBag(); + /// protected virtual void ReleaseBuffers() { } } + /// + /// A base class for formatter-specific implementations. + /// protected abstract class JsonRpcResultBase : JsonRpcResult, IJsonRpcMessageBufferManager, IMessageWithTopLevelPropertyBag { + /// + /// Gets or sets the top-level property bag for this message. + /// [Newtonsoft.Json.JsonIgnore] [IgnoreDataMember] public TopLevelPropertyBagBase? TopLevelPropertyBag { get; set; } @@ -423,6 +478,7 @@ void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message this.ReleaseBuffers(); } + /// public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) { TopLevelPropertyBagBase.ValidatePropertyName(name); @@ -431,6 +487,7 @@ public override bool TrySetTopLevelProperty(string name, [MaybeNull] T value) return this.TopLevelPropertyBag is not null; } + /// public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) { TopLevelPropertyBagBase.ValidatePropertyName(name); @@ -438,8 +495,10 @@ public override bool TryGetTopLevelProperty(string name, [MaybeNull] out T va return this.TopLevelPropertyBag?.TryGetTopLevelProperty(name, out value) is true; } + /// protected abstract TopLevelPropertyBagBase? CreateTopLevelPropertyBag(); + /// protected virtual void ReleaseBuffers() { } diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index f18b0661..3dcfdb53 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -716,11 +716,11 @@ internal void WriteProperties(JToken envelope) } else { - foreach (KeyValuePair property in this.OutboundProperties) + foreach (KeyValuePair property in this.OutboundProperties) { if (envelope[property.Key] is null) { - envelope[property.Key] = property.Value.Item2 is null ? JValue.CreateNull() : JToken.FromObject(property.Value.Item2, this.jsonSerializer); + envelope[property.Key] = property.Value.Value is null ? JValue.CreateNull() : JToken.FromObject(property.Value.Value, this.jsonSerializer); } } } diff --git a/src/StreamJsonRpc/MessagePackFormatter.cs b/src/StreamJsonRpc/MessagePackFormatter.cs index 70c45b31..30b7ac80 100644 --- a/src/StreamJsonRpc/MessagePackFormatter.cs +++ b/src/StreamJsonRpc/MessagePackFormatter.cs @@ -2103,7 +2103,7 @@ internal TopLevelPropertyBag(MessagePackSerializerOptions serializerOptions) this.serializerOptions = serializerOptions; } - internal int PropertyCount => this.OutboundProperties?.Count ?? this.inboundUnknownProperties?.Count ?? 0; + internal int PropertyCount => this.inboundUnknownProperties?.Count ?? this.OutboundProperties?.Count ?? 0; /// /// Writes the properties tracked by this collection to a messagepack writer. @@ -2111,15 +2111,27 @@ internal TopLevelPropertyBag(MessagePackSerializerOptions serializerOptions) /// The writer to use. internal void WriteProperties(ref MessagePackWriter writer) { - if (this.OutboundProperties is null) + if (this.inboundUnknownProperties is not null) { - throw new InvalidOperationException(Resources.OutboundMessageOnly); - } + // We're actually re-transmitting an incoming message (remote target feature). + // We need to copy all the properties that were in the original message. + // Don't implement this without enabling the tests for the scenario found in JsonRpcRemoteTargetMessagePackFormatterTests.cs. + // The tests fail for reasons even without this support, so there's work to do beyond just implementing this. + throw new NotImplementedException(); - foreach (KeyValuePair entry in this.OutboundProperties) + ////foreach (KeyValuePair> entry in this.inboundUnknownProperties) + ////{ + //// writer.Write(entry.Key); + //// writer.Write(entry.Value); + ////} + } + else { - writer.Write(entry.Key); - MessagePackSerializer.Serialize(entry.Value.Item1, ref writer, entry.Value.Item2, this.serializerOptions); + foreach (KeyValuePair entry in this.OutboundProperties) + { + writer.Write(entry.Key); + MessagePackSerializer.Serialize(entry.Value.DeclaredType, ref writer, entry.Value.Value, this.serializerOptions); + } } } diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index fe475ffe..cac69af0 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -357,10 +357,21 @@ internal TopLevelPropertyBag(JsonSerializerOptions jsonSerializerOptions) internal void WriteProperties(Utf8JsonWriter writer) { - foreach (KeyValuePair property in this.OutboundProperties) + if (this.incomingMessage is not null) { - writer.WritePropertyName(property.Key); - JsonSerializer.Serialize(writer, property.Value.Item2, this.jsonSerializerOptions); + // We're actually re-transmitting an incoming message (remote target feature). + // We need to copy all the properties that were in the original message. + // Don't implement this without enabling the tests for the scenario found in JsonRpcRemoteTargetSystemTextJsonFormatterTests.cs. + // The tests fail for reasons even without this support, so there's work to do beyond just implementing this. + throw new NotImplementedException(); + } + else + { + foreach (KeyValuePair property in this.OutboundProperties) + { + writer.WritePropertyName(property.Key); + JsonSerializer.Serialize(writer, property.Value.Value, this.jsonSerializerOptions); + } } } diff --git a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt index a4fe3cab..718445a2 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,13 @@ +abstract StreamJsonRpc.FormatterBase.JsonRpcErrorBase.CreateTopLevelPropertyBag() -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +abstract StreamJsonRpc.FormatterBase.JsonRpcRequestBase.CreateTopLevelPropertyBag() -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +abstract StreamJsonRpc.FormatterBase.JsonRpcResultBase.CreateTopLevelPropertyBag() -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +abstract StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase.TryGetTopLevelProperty(string! name, out T value) -> bool +override StreamJsonRpc.FormatterBase.JsonRpcErrorBase.TryGetTopLevelProperty(string! name, out T value) -> bool +override StreamJsonRpc.FormatterBase.JsonRpcErrorBase.TrySetTopLevelProperty(string! name, T value) -> bool +override StreamJsonRpc.FormatterBase.JsonRpcRequestBase.TryGetTopLevelProperty(string! name, out T value) -> bool +override StreamJsonRpc.FormatterBase.JsonRpcRequestBase.TrySetTopLevelProperty(string! name, T value) -> bool +override StreamJsonRpc.FormatterBase.JsonRpcResultBase.TryGetTopLevelProperty(string! name, out T value) -> bool +override StreamJsonRpc.FormatterBase.JsonRpcResultBase.TrySetTopLevelProperty(string! name, T value) -> bool static StreamJsonRpc.Reflection.MessageFormatterProgressTracker.CanDeserialize(System.Type! objectType) -> bool static StreamJsonRpc.Reflection.MessageFormatterProgressTracker.CanSerialize(System.Type! objectType) -> bool StreamJsonRpc.FormatterBase @@ -12,7 +22,22 @@ StreamJsonRpc.FormatterBase.DuplexPipeTracker.get -> StreamJsonRpc.Reflection.Me StreamJsonRpc.FormatterBase.EnumerableTracker.get -> StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker! StreamJsonRpc.FormatterBase.FormatterBase() -> void StreamJsonRpc.FormatterBase.FormatterProgressTracker.get -> StreamJsonRpc.Reflection.MessageFormatterProgressTracker! +StreamJsonRpc.FormatterBase.IMessageWithTopLevelPropertyBag +StreamJsonRpc.FormatterBase.IMessageWithTopLevelPropertyBag.TopLevelPropertyBag.get -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +StreamJsonRpc.FormatterBase.IMessageWithTopLevelPropertyBag.TopLevelPropertyBag.set -> void StreamJsonRpc.FormatterBase.JsonRpc.get -> StreamJsonRpc.JsonRpc? +StreamJsonRpc.FormatterBase.JsonRpcErrorBase +StreamJsonRpc.FormatterBase.JsonRpcErrorBase.JsonRpcErrorBase() -> void +StreamJsonRpc.FormatterBase.JsonRpcErrorBase.TopLevelPropertyBag.get -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +StreamJsonRpc.FormatterBase.JsonRpcErrorBase.TopLevelPropertyBag.set -> void +StreamJsonRpc.FormatterBase.JsonRpcRequestBase +StreamJsonRpc.FormatterBase.JsonRpcRequestBase.JsonRpcRequestBase() -> void +StreamJsonRpc.FormatterBase.JsonRpcRequestBase.TopLevelPropertyBag.get -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +StreamJsonRpc.FormatterBase.JsonRpcRequestBase.TopLevelPropertyBag.set -> void +StreamJsonRpc.FormatterBase.JsonRpcResultBase +StreamJsonRpc.FormatterBase.JsonRpcResultBase.JsonRpcResultBase() -> void +StreamJsonRpc.FormatterBase.JsonRpcResultBase.TopLevelPropertyBag.get -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +StreamJsonRpc.FormatterBase.JsonRpcResultBase.TopLevelPropertyBag.set -> void StreamJsonRpc.FormatterBase.MultiplexingStream.get -> Nerdbank.Streams.MultiplexingStream? StreamJsonRpc.FormatterBase.MultiplexingStream.set -> void StreamJsonRpc.FormatterBase.SerializationTracking @@ -21,6 +46,9 @@ StreamJsonRpc.FormatterBase.SerializationTracking.SerializationTracking() -> voi StreamJsonRpc.FormatterBase.SerializationTracking.SerializationTracking(StreamJsonRpc.FormatterBase! formatter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void StreamJsonRpc.FormatterBase.SerializingMessageWithId.get -> StreamJsonRpc.RequestId StreamJsonRpc.FormatterBase.SerializingRequest.get -> bool +StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase +StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase.OutboundProperties.get -> System.Collections.Generic.Dictionary! +StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase.TopLevelPropertyBagBase(bool isOutbound) -> void StreamJsonRpc.FormatterBase.TrackDeserialization(StreamJsonRpc.Protocol.JsonRpcMessage! message, System.ReadOnlySpan parameters = default(System.ReadOnlySpan)) -> StreamJsonRpc.FormatterBase.DeserializationTracking StreamJsonRpc.FormatterBase.TrackSerialization(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> StreamJsonRpc.FormatterBase.SerializationTracking StreamJsonRpc.SystemTextJsonFormatter @@ -32,4 +60,7 @@ StreamJsonRpc.SystemTextJsonFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpc StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.set -> void StreamJsonRpc.SystemTextJsonFormatter.Serialize(System.Buffers.IBufferWriter! bufferWriter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void -virtual StreamJsonRpc.FormatterBase.Dispose(bool disposing) -> void \ No newline at end of file +virtual StreamJsonRpc.FormatterBase.Dispose(bool disposing) -> void +virtual StreamJsonRpc.FormatterBase.JsonRpcErrorBase.ReleaseBuffers() -> void +virtual StreamJsonRpc.FormatterBase.JsonRpcRequestBase.ReleaseBuffers() -> void +virtual StreamJsonRpc.FormatterBase.JsonRpcResultBase.ReleaseBuffers() -> void \ No newline at end of file diff --git a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt index a4fe3cab..718445a2 100644 --- a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt @@ -1,3 +1,13 @@ +abstract StreamJsonRpc.FormatterBase.JsonRpcErrorBase.CreateTopLevelPropertyBag() -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +abstract StreamJsonRpc.FormatterBase.JsonRpcRequestBase.CreateTopLevelPropertyBag() -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +abstract StreamJsonRpc.FormatterBase.JsonRpcResultBase.CreateTopLevelPropertyBag() -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +abstract StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase.TryGetTopLevelProperty(string! name, out T value) -> bool +override StreamJsonRpc.FormatterBase.JsonRpcErrorBase.TryGetTopLevelProperty(string! name, out T value) -> bool +override StreamJsonRpc.FormatterBase.JsonRpcErrorBase.TrySetTopLevelProperty(string! name, T value) -> bool +override StreamJsonRpc.FormatterBase.JsonRpcRequestBase.TryGetTopLevelProperty(string! name, out T value) -> bool +override StreamJsonRpc.FormatterBase.JsonRpcRequestBase.TrySetTopLevelProperty(string! name, T value) -> bool +override StreamJsonRpc.FormatterBase.JsonRpcResultBase.TryGetTopLevelProperty(string! name, out T value) -> bool +override StreamJsonRpc.FormatterBase.JsonRpcResultBase.TrySetTopLevelProperty(string! name, T value) -> bool static StreamJsonRpc.Reflection.MessageFormatterProgressTracker.CanDeserialize(System.Type! objectType) -> bool static StreamJsonRpc.Reflection.MessageFormatterProgressTracker.CanSerialize(System.Type! objectType) -> bool StreamJsonRpc.FormatterBase @@ -12,7 +22,22 @@ StreamJsonRpc.FormatterBase.DuplexPipeTracker.get -> StreamJsonRpc.Reflection.Me StreamJsonRpc.FormatterBase.EnumerableTracker.get -> StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker! StreamJsonRpc.FormatterBase.FormatterBase() -> void StreamJsonRpc.FormatterBase.FormatterProgressTracker.get -> StreamJsonRpc.Reflection.MessageFormatterProgressTracker! +StreamJsonRpc.FormatterBase.IMessageWithTopLevelPropertyBag +StreamJsonRpc.FormatterBase.IMessageWithTopLevelPropertyBag.TopLevelPropertyBag.get -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +StreamJsonRpc.FormatterBase.IMessageWithTopLevelPropertyBag.TopLevelPropertyBag.set -> void StreamJsonRpc.FormatterBase.JsonRpc.get -> StreamJsonRpc.JsonRpc? +StreamJsonRpc.FormatterBase.JsonRpcErrorBase +StreamJsonRpc.FormatterBase.JsonRpcErrorBase.JsonRpcErrorBase() -> void +StreamJsonRpc.FormatterBase.JsonRpcErrorBase.TopLevelPropertyBag.get -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +StreamJsonRpc.FormatterBase.JsonRpcErrorBase.TopLevelPropertyBag.set -> void +StreamJsonRpc.FormatterBase.JsonRpcRequestBase +StreamJsonRpc.FormatterBase.JsonRpcRequestBase.JsonRpcRequestBase() -> void +StreamJsonRpc.FormatterBase.JsonRpcRequestBase.TopLevelPropertyBag.get -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +StreamJsonRpc.FormatterBase.JsonRpcRequestBase.TopLevelPropertyBag.set -> void +StreamJsonRpc.FormatterBase.JsonRpcResultBase +StreamJsonRpc.FormatterBase.JsonRpcResultBase.JsonRpcResultBase() -> void +StreamJsonRpc.FormatterBase.JsonRpcResultBase.TopLevelPropertyBag.get -> StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase? +StreamJsonRpc.FormatterBase.JsonRpcResultBase.TopLevelPropertyBag.set -> void StreamJsonRpc.FormatterBase.MultiplexingStream.get -> Nerdbank.Streams.MultiplexingStream? StreamJsonRpc.FormatterBase.MultiplexingStream.set -> void StreamJsonRpc.FormatterBase.SerializationTracking @@ -21,6 +46,9 @@ StreamJsonRpc.FormatterBase.SerializationTracking.SerializationTracking() -> voi StreamJsonRpc.FormatterBase.SerializationTracking.SerializationTracking(StreamJsonRpc.FormatterBase! formatter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void StreamJsonRpc.FormatterBase.SerializingMessageWithId.get -> StreamJsonRpc.RequestId StreamJsonRpc.FormatterBase.SerializingRequest.get -> bool +StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase +StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase.OutboundProperties.get -> System.Collections.Generic.Dictionary! +StreamJsonRpc.FormatterBase.TopLevelPropertyBagBase.TopLevelPropertyBagBase(bool isOutbound) -> void StreamJsonRpc.FormatterBase.TrackDeserialization(StreamJsonRpc.Protocol.JsonRpcMessage! message, System.ReadOnlySpan parameters = default(System.ReadOnlySpan)) -> StreamJsonRpc.FormatterBase.DeserializationTracking StreamJsonRpc.FormatterBase.TrackSerialization(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> StreamJsonRpc.FormatterBase.SerializationTracking StreamJsonRpc.SystemTextJsonFormatter @@ -32,4 +60,7 @@ StreamJsonRpc.SystemTextJsonFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpc StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.set -> void StreamJsonRpc.SystemTextJsonFormatter.Serialize(System.Buffers.IBufferWriter! bufferWriter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void -virtual StreamJsonRpc.FormatterBase.Dispose(bool disposing) -> void \ No newline at end of file +virtual StreamJsonRpc.FormatterBase.Dispose(bool disposing) -> void +virtual StreamJsonRpc.FormatterBase.JsonRpcErrorBase.ReleaseBuffers() -> void +virtual StreamJsonRpc.FormatterBase.JsonRpcRequestBase.ReleaseBuffers() -> void +virtual StreamJsonRpc.FormatterBase.JsonRpcResultBase.ReleaseBuffers() -> void \ No newline at end of file diff --git a/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetJsonMessageFormatterTests.cs b/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetJsonMessageFormatterTests.cs new file mode 100644 index 00000000..927c1353 --- /dev/null +++ b/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetJsonMessageFormatterTests.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using StreamJsonRpc; + +public class JsonRpcRemoteTargetJsonMessageFormatterTests : JsonRpcRemoteTargetTests +{ + public JsonRpcRemoteTargetJsonMessageFormatterTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override IJsonRpcMessageFormatter CreateFormatter() => new JsonMessageFormatter(); +} diff --git a/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetMessagePackFormatterTests.cs b/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetMessagePackFormatterTests.cs new file mode 100644 index 00000000..c42de92b --- /dev/null +++ b/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetMessagePackFormatterTests.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +/* This test class enables a set of tests for a scenario that doesn't work on the MessagePackFormatter today. + +using StreamJsonRpc; + +public class JsonRpcRemoteTargetMessagePackFormatterTests : JsonRpcRemoteTargetTests +{ + public JsonRpcRemoteTargetMessagePackFormatterTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override IJsonRpcMessageFormatter CreateFormatter() => new MessagePackFormatter(); + + protected override IJsonRpcMessageHandler CreateHandler(Stream sending, Stream receiving) => new LengthHeaderMessageHandler(sending, receiving, this.CreateFormatter()); +} +*/ diff --git a/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetSystemTextJsonFormatterTests.cs b/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetSystemTextJsonFormatterTests.cs new file mode 100644 index 00000000..00f28386 --- /dev/null +++ b/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetSystemTextJsonFormatterTests.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +/* This test class enables a set of tests for a scenario that doesn't work on the SystemTextJsonFormatter today. + +using StreamJsonRpc; + +public class JsonRpcRemoteTargetSystemTextJsonFormatterTests : JsonRpcRemoteTargetTests +{ + public JsonRpcRemoteTargetSystemTextJsonFormatterTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override IJsonRpcMessageFormatter CreateFormatter() => new SystemTextJsonFormatter(); +} + +*/ diff --git a/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetTests.cs b/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetTests.cs index 58e163ba..fd5574ca 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcRemoteTargetTests.cs @@ -4,7 +4,7 @@ using Xunit; using Xunit.Abstractions; -public class JsonRpcRemoteTargetTests : InteropTestBase +public abstract class JsonRpcRemoteTargetTests : InteropTestBase { private readonly JsonRpc localRpc; private readonly RemoteTargetJsonRpc originRpc; @@ -22,7 +22,7 @@ public JsonRpcRemoteTargetTests(ITestOutputHelper logger) * remoteRpc* is the RPC connection from remote to local. */ var streams = FullDuplexStream.CreatePair(); - this.localRpc = new JsonRpc(streams.Item2); + this.localRpc = new JsonRpc(this.CreateHandler(streams.Item2, streams.Item2)); this.localRpc.AllowModificationWhileListening = true; this.localRpc.StartListening(); @@ -45,10 +45,10 @@ public JsonRpcRemoteTargetTests(ITestOutputHelper logger) var remoteTarget2 = JsonRpc.Attach(remoteClientStream2, remoteClientStream2, new LocalRelayTarget()); remoteTarget2.AllowModificationWhileListening = true; - this.remoteRpc1 = new JsonRpc(remoteServerStream1, remoteServerStream1, new RemoteTargetOne()); + this.remoteRpc1 = new JsonRpc(this.CreateHandler(remoteServerStream1, remoteServerStream1), new RemoteTargetOne()); this.remoteRpc1.StartListening(); - this.remoteRpc2 = new JsonRpc(remoteServerStream2, remoteServerStream2, new RemoteTargetTwo()); + this.remoteRpc2 = new JsonRpc(this.CreateHandler(remoteServerStream2, remoteServerStream2), new RemoteTargetTwo()); this.remoteRpc2.StartListening(); this.localRpc.AddLocalRpcTarget(new LocalOriginTarget(this.remoteTarget1)); @@ -244,6 +244,10 @@ public async Task VerifyMethodOrderingIsNotGuaranteedAfterYielding() Assert.Equal(2, remoteCallTask.Result); } + protected virtual IJsonRpcMessageHandler CreateHandler(Stream sending, Stream receiving) => new HeaderDelimitedMessageHandler(sending, receiving, this.CreateFormatter()); + + protected abstract IJsonRpcMessageFormatter CreateFormatter(); + public static class Counter { public static int CurrentCount; diff --git a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj index cb04a331..9097dd0e 100644 --- a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj +++ b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj @@ -26,6 +26,9 @@ + + + diff --git a/test/StreamJsonRpc.Tests/Usings.cs b/test/StreamJsonRpc.Tests/Usings.cs new file mode 100644 index 00000000..73b96bef --- /dev/null +++ b/test/StreamJsonRpc.Tests/Usings.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +global using Xunit.Abstractions; From a1eac9722ed09181b2ae0a4f3ea2b90a6ac12c82 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 21 Apr 2023 13:05:03 -0600 Subject: [PATCH 19/35] Fix outbound argument count --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index cac69af0..4a11c650 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -392,7 +392,7 @@ private class JsonRpcRequest : JsonRpcRequestBase { private readonly SystemTextJsonFormatter formatter; - private int argumentCount; + private int? argumentCount; private JsonElement? jsonArguments; @@ -401,7 +401,7 @@ internal JsonRpcRequest(SystemTextJsonFormatter formatter) this.formatter = formatter; } - public override int ArgumentCount => this.argumentCount; + public override int ArgumentCount => this.argumentCount ?? base.ArgumentCount; internal JsonElement? JsonArguments { From ae99ea25072c6702da82a45ab098d4bf61e1d276 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 21 Apr 2023 15:29:09 -0600 Subject: [PATCH 20/35] Add exception serialization support --- src/StreamJsonRpc/JsonMessageFormatter.cs | 2 +- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 224 +++++++++++++++++- .../netstandard2.0/PublicAPI.Unshipped.txt | 1 + .../netstandard2.1/PublicAPI.Unshipped.txt | 1 + .../JsonRpcJsonHeadersTests.cs | 8 +- .../JsonRpcMessagePackLengthTests.cs | 6 +- .../JsonRpcSystemTextJsonHeadersTests.cs | 53 +++-- test/StreamJsonRpc.Tests/JsonRpcTests.cs | 12 +- test/StreamJsonRpc.Tests/Usings.cs | 3 + 9 files changed, 271 insertions(+), 39 deletions(-) diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index 3dcfdb53..c687cb31 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -860,7 +860,7 @@ public override T GetResult() } catch (Exception exception) { - throw new JsonSerializationException(string.Format(CultureInfo.CurrentCulture, Resources.FailureDeserializingRpcResult, typeof(T).Name, exception.GetType().Name, exception.Message)); + throw new JsonSerializationException(string.Format(CultureInfo.CurrentCulture, Resources.FailureDeserializingRpcResult, typeof(T).Name, exception.GetType().Name, exception.Message), exception); } } diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 4a11c650..e207c4c3 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -2,12 +2,15 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Buffers; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Reflection; using System.Runtime.ExceptionServices; using System.Runtime.Serialization; using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using StreamJsonRpc.Protocol; @@ -65,10 +68,11 @@ public Encoding Encoding } /// - /// Sets the options to use when serializing and deserializing JSON containing user data. + /// Gets or sets the options to use when serializing and deserializing JSON containing user data. /// public JsonSerializerOptions JsonSerializerOptions { + get => this.massagedUserDataSerializerOptions; set => this.massagedUserDataSerializerOptions = this.MassageUserDataSerializerOptions(new(value)); } @@ -83,6 +87,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding throw new NotSupportedException("Only our default encoding is supported."); } + // TODO: dispose of the document when we're done with it. This means each JsonRpcMessage will need to dispose of it in ReleaseBuffers since they capture pieces of it. JsonDocument document = JsonDocument.Parse(contentBuffer, DocumentOptions); if (document.RootElement.ValueKind != JsonValueKind.Object) { @@ -298,6 +303,9 @@ private JsonSerializerOptions MassageUserDataSerializerOptions(JsonSerializerOpt // Add support for exotic types. options.Converters.Add(new ProgressConverterFactory(this)); + // Add support for serializing exceptions. + options.Converters.Add(new ExceptionConverter(this)); + return options; } @@ -470,7 +478,19 @@ public override bool TryGetArgumentByNameOrIndex(string? name, int position, Typ { using (this.formatter.TrackDeserialization(this)) { - value = valueElement?.Deserialize(typeHint ?? typeof(object), this.formatter.massagedUserDataSerializerOptions); + try + { + value = valueElement?.Deserialize(typeHint ?? typeof(object), this.formatter.massagedUserDataSerializerOptions); + } + catch (Exception ex) + { + if (this.formatter.JsonRpc?.TraceSource.Switch.ShouldTrace(TraceEventType.Warning) ?? false) + { + this.formatter.JsonRpc.TraceSource.TraceEvent(TraceEventType.Warning, (int)JsonRpc.TraceEvents.MethodArgumentDeserializationFailure, Resources.FailureDeserializingRpcArgument, name, position, typeHint, ex); + } + + throw new RpcArgumentDeserializationException(name, position, typeHint, ex); + } } } catch (JsonException ex) @@ -553,7 +573,7 @@ protected internal override void SetExpectedResultType(Type resultType) catch (Exception ex) { // This was a best effort anyway. We'll throw again later at a more convenient time for JsonRpc. - this.resultDeserializationException = ex; + this.resultDeserializationException = new JsonException(string.Format(CultureInfo.CurrentCulture, Resources.FailureDeserializingRpcResult, resultType.Name, ex.GetType().Name, ex.Message), ex); } } @@ -726,6 +746,204 @@ public override void Write(Utf8JsonWriter writer, IProgress? value, JsonSeria } } + private class ExceptionConverter : JsonConverter + { + /// + /// Tracks recursion count while serializing or deserializing an exception. + /// + private static ThreadLocal exceptionRecursionCounter = new(); + + private readonly SystemTextJsonFormatter formatter; + + public ExceptionConverter(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override bool CanConvert(Type typeToConvert) => typeof(Exception).IsAssignableFrom(typeToConvert); + + public override Exception? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Assumes.NotNull(this.formatter.JsonRpc); + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + exceptionRecursionCounter.Value++; + try + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new InvalidOperationException("Expected a StartObject token."); + } + + if (exceptionRecursionCounter.Value > this.formatter.JsonRpc.ExceptionOptions.RecursionLimit) + { + // Exception recursion has gone too deep. Skip this value and return null as if there were no inner exception. + // Note that in skipping, the parser may use recursion internally and may still throw if its own limits are exceeded. + reader.Skip(); + return null; + } + + JsonNode? jsonNode = JsonNode.Parse(ref reader) ?? throw new JsonException("Unexpected null"); + SerializationInfo? info = new SerializationInfo(typeToConvert, new JsonConverterFormatter(this.formatter.massagedUserDataSerializerOptions)); + foreach (KeyValuePair property in jsonNode.AsObject()) + { + info.AddSafeValue(property.Key, property.Value); + } + + return ExceptionSerializationHelpers.Deserialize(this.formatter.JsonRpc, info, this.formatter.JsonRpc?.TraceSource); + } + finally + { + exceptionRecursionCounter.Value--; + } + } + + public override void Write(Utf8JsonWriter writer, Exception? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + // We have to guard our own recursion because the serializer has no visibility into inner exceptions. + // Each exception in the russian doll is a new serialization job from its perspective. + exceptionRecursionCounter.Value++; + try + { + if (exceptionRecursionCounter.Value > this.formatter.JsonRpc?.ExceptionOptions.RecursionLimit) + { + // Exception recursion has gone too deep. Skip this value and write null as if there were no inner exception. + writer.WriteNullValue(); + return; + } + + SerializationInfo info = new SerializationInfo(value.GetType(), new JsonConverterFormatter(this.formatter.massagedUserDataSerializerOptions)); + ExceptionSerializationHelpers.Serialize(value, info); + writer.WriteStartObject(); + foreach (SerializationEntry element in info.GetSafeMembers()) + { + writer.WritePropertyName(element.Name); + JsonSerializer.Serialize(writer, element.Value, options); + } + + writer.WriteEndObject(); + } + catch (Exception ex) + { + throw new JsonException(ex.Message, ex); + } + finally + { + exceptionRecursionCounter.Value--; + } + } + } + + private class JsonConverterFormatter : IFormatterConverter + { + private readonly JsonSerializerOptions serializerOptions; + + internal JsonConverterFormatter(JsonSerializerOptions serializerOptions) + { + this.serializerOptions = serializerOptions; + } + + public object? Convert(object value, Type type) + { + var jsonValue = (JsonNode?)value; + if (jsonValue is null) + { + return null; + } + + if (type == typeof(System.Collections.IDictionary)) + { + return DeserializePrimitive(jsonValue); + } + + return jsonValue.Deserialize(type, this.serializerOptions)!; + } + + public object Convert(object value, TypeCode typeCode) + { + return typeCode switch + { + TypeCode.Object => ((JsonNode)value).Deserialize(typeof(object), this.serializerOptions)!, + _ => ExceptionSerializationHelpers.Convert(this, value, typeCode), + }; + } + + public bool ToBoolean(object value) => ((JsonNode)value).GetValue(); + + public byte ToByte(object value) => ((JsonNode)value).GetValue(); + + public char ToChar(object value) => ((JsonNode)value).GetValue(); + + public DateTime ToDateTime(object value) => ((JsonNode)value).GetValue(); + + public decimal ToDecimal(object value) => ((JsonNode)value).GetValue(); + + public double ToDouble(object value) => ((JsonNode)value).GetValue(); + + public short ToInt16(object value) => ((JsonNode)value).GetValue(); + + public int ToInt32(object value) => ((JsonNode)value).GetValue(); + + public long ToInt64(object value) => ((JsonNode)value).GetValue(); + + public sbyte ToSByte(object value) => ((JsonNode)value).GetValue(); + + public float ToSingle(object value) => ((JsonNode)value).GetValue(); + + public string? ToString(object value) => ((JsonNode)value).GetValue(); + + public ushort ToUInt16(object value) => ((JsonNode)value).GetValue(); + + public uint ToUInt32(object value) => ((JsonNode)value).GetValue(); + + public ulong ToUInt64(object value) => ((JsonNode)value).GetValue(); + + private static object? DeserializePrimitive(JsonNode? node) + { + return node switch + { + JsonObject o => DeserializeObjectAsDictionary(o), + JsonValue v => DeserializePrimitive(v.GetValue()), + JsonArray a => a.Select(DeserializePrimitive).ToArray(), + null => null, + _ => throw new NotSupportedException("Unrecognized node type: " + node.GetType().Name), + }; + } + + private static Dictionary DeserializeObjectAsDictionary(JsonNode jsonNode) + { + Dictionary dictionary = new(); + foreach (KeyValuePair property in jsonNode.AsObject()) + { + dictionary.Add(property.Key, DeserializePrimitive(property.Value)); + } + + return dictionary; + } + + private static object? DeserializePrimitive(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.TryGetInt32(out int intValue) ? intValue : element.GetInt64(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => throw new NotSupportedException(), + }; + } + } + private class DataContractResolver : IJsonTypeInfoResolver { private readonly Dictionary typeInfoCache = new(); diff --git a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt index 718445a2..423af780 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt @@ -57,6 +57,7 @@ StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequenc StreamJsonRpc.SystemTextJsonFormatter.Encoding.get -> System.Text.Encoding! StreamJsonRpc.SystemTextJsonFormatter.Encoding.set -> void StreamJsonRpc.SystemTextJsonFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> object! +StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.get -> System.Text.Json.JsonSerializerOptions! StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.set -> void StreamJsonRpc.SystemTextJsonFormatter.Serialize(System.Buffers.IBufferWriter! bufferWriter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void diff --git a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt index 718445a2..423af780 100644 --- a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt @@ -57,6 +57,7 @@ StreamJsonRpc.SystemTextJsonFormatter.Deserialize(System.Buffers.ReadOnlySequenc StreamJsonRpc.SystemTextJsonFormatter.Encoding.get -> System.Text.Encoding! StreamJsonRpc.SystemTextJsonFormatter.Encoding.set -> void StreamJsonRpc.SystemTextJsonFormatter.GetJsonText(StreamJsonRpc.Protocol.JsonRpcMessage! message) -> object! +StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.get -> System.Text.Json.JsonSerializerOptions! StreamJsonRpc.SystemTextJsonFormatter.JsonSerializerOptions.set -> void StreamJsonRpc.SystemTextJsonFormatter.Serialize(System.Buffers.IBufferWriter! bufferWriter, StreamJsonRpc.Protocol.JsonRpcMessage! message) -> void StreamJsonRpc.SystemTextJsonFormatter.SystemTextJsonFormatter() -> void diff --git a/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs b/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs index ee901682..e4fa9325 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs @@ -1,15 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Runtime.Serialization; using System.Text; using Microsoft.VisualStudio.Threading; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StreamJsonRpc; -using StreamJsonRpc.Protocol; -using Xunit; -using Xunit.Abstractions; public class JsonRpcJsonHeadersTests : JsonRpcTests { @@ -18,6 +12,8 @@ public JsonRpcJsonHeadersTests(ITestOutputHelper logger) { } + protected override Type FormatterExceptionType => typeof(JsonSerializationException); + [Fact] public async Task CustomJsonConvertersAreNotAppliedToBaseMessage() { diff --git a/test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs b/test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs index f6ce5fd3..c510e812 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs @@ -6,10 +6,6 @@ using MessagePack.Formatters; using MessagePack.Resolvers; using Microsoft.VisualStudio.Threading; -using StreamJsonRpc; -using StreamJsonRpc.Protocol; -using Xunit; -using Xunit.Abstractions; public class JsonRpcMessagePackLengthTests : JsonRpcTests { @@ -33,6 +29,8 @@ internal interface IMessagePackServer Task IsExtensionArgNonNull(CustomExtensionType extensionValue); } + protected override Type FormatterExceptionType => typeof(MessagePackSerializationException); + [Fact] public override async Task CanPassAndCallPrivateMethodsObjects() { diff --git a/test/StreamJsonRpc.Tests/JsonRpcSystemTextJsonHeadersTests.cs b/test/StreamJsonRpc.Tests/JsonRpcSystemTextJsonHeadersTests.cs index d601a3a9..6071d9f1 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcSystemTextJsonHeadersTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcSystemTextJsonHeadersTests.cs @@ -1,15 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Runtime.Serialization; -using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.VisualStudio.Threading; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StreamJsonRpc; -using StreamJsonRpc.Protocol; -using Xunit; -using Xunit.Abstractions; public class JsonRpcSystemTextJsonHeadersTests : JsonRpcTests { @@ -18,13 +12,40 @@ public JsonRpcSystemTextJsonHeadersTests(ITestOutputHelper logger) { } + protected override Type FormatterExceptionType => typeof(JsonException); + + [Fact] + public override async Task CanPassExceptionFromServer_ErrorData() + { + RemoteInvocationException exception = await Assert.ThrowsAnyAsync(() => this.clientRpc.InvokeAsync(nameof(Server.MethodThatThrowsUnauthorizedAccessException))); + Assert.Equal((int)JsonRpcErrorCode.InvocationError, exception.ErrorCode); + + var errorData = Assert.IsType(exception.ErrorData); + Assert.NotNull(errorData.StackTrace); + Assert.StrictEqual(COR_E_UNAUTHORIZEDACCESS, errorData.HResult); + } + protected override void InitializeFormattersAndHandlers(bool controlledFlushingClient) { this.clientMessageFormatter = new SystemTextJsonFormatter { + JsonSerializerOptions = + { + Converters = + { + new TypeThrowsWhenDeserializedConverter(), + }, + }, }; this.serverMessageFormatter = new SystemTextJsonFormatter { + JsonSerializerOptions = + { + Converters = + { + new TypeThrowsWhenDeserializedConverter(), + }, + }, }; this.serverMessageHandler = new HeaderDelimitedMessageHandler(this.serverStream, this.serverStream, this.serverMessageFormatter); @@ -52,21 +73,17 @@ protected override async ValueTask FlushAsync(CancellationToken cancellationToke } } - private class StringBase64Converter : JsonConverter + private class TypeThrowsWhenDeserializedConverter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(string); - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override TypeThrowsWhenDeserialized? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string decoded = Encoding.UTF8.GetString(Convert.FromBase64String((string)reader.Value!)); - return decoded; + throw CreateExceptionToBeThrownByDeserializer(); } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, TypeThrowsWhenDeserialized value, JsonSerializerOptions options) { - var stringValue = (string?)value; - var encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(stringValue!)); - writer.WriteValue(encoded); + writer.WriteStartObject(); + writer.WriteEndObject(); } } } diff --git a/test/StreamJsonRpc.Tests/JsonRpcTests.cs b/test/StreamJsonRpc.Tests/JsonRpcTests.cs index 2591c347..c161ae18 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -13,10 +13,6 @@ using Nerdbank.Streams; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using StreamJsonRpc; -using StreamJsonRpc.Protocol; -using Xunit; -using Xunit.Abstractions; public abstract class JsonRpcTests : TestBase { @@ -101,6 +97,8 @@ private interface IServerDerived : IServer protected bool IsTypeNameHandlingEnabled => this.clientMessageFormatter is JsonMessageFormatter { JsonSerializer: { TypeNameHandling: TypeNameHandling.Objects } }; + protected abstract Type FormatterExceptionType { get; } + [Fact] public async Task AddLocalRpcTarget_OfT_InterfaceOnly() { @@ -1386,7 +1384,7 @@ public async Task ReportProgressWithUnserializableData_LeavesTraceEvidence() public async Task NotifyAsync_LeavesTraceEvidenceOnFailure() { var exception = await Assert.ThrowsAnyAsync(() => this.clientRpc.NotifyAsync("DoesNotMatter", new TypeThrowsWhenSerialized())); - Assert.True(exception is JsonSerializationException || exception is MessagePackSerializationException); + Assert.IsAssignableFrom(this.FormatterExceptionType, exception); // Verify that the trace explains what went wrong with the original exception message. while (!this.clientTraces.Messages.Any(m => m.Contains("Can't touch this"))) @@ -2196,7 +2194,7 @@ public async Task FormatterFatalException() public async Task ReturnTypeThrowsOnDeserialization() { var ex = await Assert.ThrowsAnyAsync(() => this.clientRpc.InvokeWithCancellationAsync(nameof(Server.GetTypeThrowsWhenDeserialized), cancellationToken: this.TimeoutToken)).WithCancellation(this.TimeoutToken); - Assert.True(ex is JsonSerializationException || ex is MessagePackSerializationException, $"Exception type was {ex.GetType().Name}"); + Assert.IsAssignableFrom(this.FormatterExceptionType, ex); } [Fact] @@ -2560,7 +2558,7 @@ public async Task NonSerializableExceptionInArgumentThrowsLocally() // Synthesize an exception message that refers to an exception type that does not exist. var exceptionToSend = new NonSerializableException(Server.ExceptionMessage); var exception = await Assert.ThrowsAnyAsync(() => this.clientRpc.InvokeWithCancellationAsync(nameof(Server.SendException), new[] { exceptionToSend }, new[] { typeof(Exception) }, this.TimeoutToken)); - Assert.True(exception is JsonSerializationException || exception is MessagePackSerializationException); + Assert.IsAssignableFrom(this.FormatterExceptionType, exception); } [Fact] diff --git a/test/StreamJsonRpc.Tests/Usings.cs b/test/StreamJsonRpc.Tests/Usings.cs index 73b96bef..d83575da 100644 --- a/test/StreamJsonRpc.Tests/Usings.cs +++ b/test/StreamJsonRpc.Tests/Usings.cs @@ -1,4 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +global using StreamJsonRpc; +global using StreamJsonRpc.Protocol; +global using Xunit; global using Xunit.Abstractions; From 24be5bef3802fb11f6f7bebac4b56ef2972101e2 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 21 Apr 2023 16:15:33 -0600 Subject: [PATCH 21/35] Fix another test --- src/StreamJsonRpc/Resources.Designer.cs | 9 + src/StreamJsonRpc/Resources.resx | 3 + src/StreamJsonRpc/SystemTextJsonFormatter.cs | 173 ++++++++++--------- 3 files changed, 102 insertions(+), 83 deletions(-) diff --git a/src/StreamJsonRpc/Resources.Designer.cs b/src/StreamJsonRpc/Resources.Designer.cs index 8eb187c8..8da7c858 100644 --- a/src/StreamJsonRpc/Resources.Designer.cs +++ b/src/StreamJsonRpc/Resources.Designer.cs @@ -564,6 +564,15 @@ internal static string RpcMethodNameNotFound { } } + /// + /// Looks up a localized string similar to An error occured during serialization.. + /// + internal static string SerializationFailure { + get { + return ResourceManager.GetString("SerializationFailure", resourceCulture); + } + } + /// /// Looks up a localized string similar to Stream has been disposed. /// diff --git a/src/StreamJsonRpc/Resources.resx b/src/StreamJsonRpc/Resources.resx index c3d75baf..4eaf998d 100644 --- a/src/StreamJsonRpc/Resources.resx +++ b/src/StreamJsonRpc/Resources.resx @@ -300,6 +300,9 @@ No method by the name '{0}' is found. + + An error occured during serialization. + Stream has been disposed diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index e207c4c3..ebf868a1 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -172,119 +172,126 @@ public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message) { using (this.TrackSerialization(message)) { - using Utf8JsonWriter writer = new(bufferWriter, WriterOptions); - writer.WriteStartObject(); - WriteVersion(); - switch (message) - { - case Protocol.JsonRpcRequest request: - WriteId(request.RequestId); - writer.WriteString(Utf8Strings.method, request.Method); - WriteArguments(request); - break; - case Protocol.JsonRpcResult result: - WriteId(result.RequestId); - WriteResult(result); - break; - case Protocol.JsonRpcError error: - WriteId(error.RequestId); - WriteError(error); - break; - default: - throw new ArgumentException("Unknown message type: " + message.GetType().Name, nameof(message)); - } - - if (message is IMessageWithTopLevelPropertyBag { TopLevelPropertyBag: TopLevelPropertyBag propertyBag }) - { - propertyBag.WriteProperties(writer); - } - - writer.WriteEndObject(); - - void WriteVersion() + try { - switch (message.Version) + using Utf8JsonWriter writer = new(bufferWriter, WriterOptions); + writer.WriteStartObject(); + WriteVersion(); + switch (message) { - case "1.0": - // The 1.0 protocol didn't include the version property at all. + case Protocol.JsonRpcRequest request: + WriteId(request.RequestId); + writer.WriteString(Utf8Strings.method, request.Method); + WriteArguments(request); break; - case "2.0": - writer.WriteString(Utf8Strings.jsonrpc, Utf8Strings.v2_0); + case Protocol.JsonRpcResult result: + WriteId(result.RequestId); + WriteResult(result); break; - default: - writer.WriteString(Utf8Strings.jsonrpc, message.Version); + case Protocol.JsonRpcError error: + WriteId(error.RequestId); + WriteError(error); break; + default: + throw new ArgumentException("Unknown message type: " + message.GetType().Name, nameof(message)); } - } - void WriteId(RequestId id) - { - if (!id.IsEmpty) + if (message is IMessageWithTopLevelPropertyBag { TopLevelPropertyBag: TopLevelPropertyBag propertyBag }) { - writer.WritePropertyName(Utf8Strings.id); - RequestIdJsonConverter.Instance.Write(writer, id, BuiltInSerializerOptions); + propertyBag.WriteProperties(writer); } - } - void WriteArguments(Protocol.JsonRpcRequest request) - { - if (request.ArgumentsList is not null) + writer.WriteEndObject(); + + void WriteVersion() { - writer.WriteStartArray(Utf8Strings.@params); - for (int i = 0; i < request.ArgumentsList.Count; i++) + switch (message.Version) { - WriteUserData(request.ArgumentsList[i]); + case "1.0": + // The 1.0 protocol didn't include the version property at all. + break; + case "2.0": + writer.WriteString(Utf8Strings.jsonrpc, Utf8Strings.v2_0); + break; + default: + writer.WriteString(Utf8Strings.jsonrpc, message.Version); + break; } + } - writer.WriteEndArray(); + void WriteId(RequestId id) + { + if (!id.IsEmpty) + { + writer.WritePropertyName(Utf8Strings.id); + RequestIdJsonConverter.Instance.Write(writer, id, BuiltInSerializerOptions); + } } - else if (request.NamedArguments is not null) + + void WriteArguments(Protocol.JsonRpcRequest request) { - writer.WriteStartObject(Utf8Strings.@params); - foreach (KeyValuePair argument in request.NamedArguments) + if (request.ArgumentsList is not null) { - writer.WritePropertyName(argument.Key); - WriteUserData(argument.Value); + writer.WriteStartArray(Utf8Strings.@params); + for (int i = 0; i < request.ArgumentsList.Count; i++) + { + WriteUserData(request.ArgumentsList[i]); + } + + writer.WriteEndArray(); } + else if (request.NamedArguments is not null) + { + writer.WriteStartObject(Utf8Strings.@params); + foreach (KeyValuePair argument in request.NamedArguments) + { + writer.WritePropertyName(argument.Key); + WriteUserData(argument.Value); + } - writer.WriteEndObject(); + writer.WriteEndObject(); + } + else if (request.Arguments is not null) + { + // This is a custom named arguments object, so we'll just serialize it as-is. + writer.WritePropertyName(Utf8Strings.@params); + WriteUserData(request.Arguments); + } } - else if (request.Arguments is not null) + + void WriteResult(Protocol.JsonRpcResult result) { - // This is a custom named arguments object, so we'll just serialize it as-is. - writer.WritePropertyName(Utf8Strings.@params); - WriteUserData(request.Arguments); + writer.WritePropertyName(Utf8Strings.result); + WriteUserData(result.Result); } - } - - void WriteResult(Protocol.JsonRpcResult result) - { - writer.WritePropertyName(Utf8Strings.result); - WriteUserData(result.Result); - } - void WriteError(Protocol.JsonRpcError error) - { - if (error.Error is null) + void WriteError(Protocol.JsonRpcError error) { - throw new ArgumentException($"{nameof(error.Error)} property must be set.", nameof(message)); + if (error.Error is null) + { + throw new ArgumentException($"{nameof(error.Error)} property must be set.", nameof(message)); + } + + writer.WriteStartObject(Utf8Strings.error); + writer.WriteNumber(Utf8Strings.code, (int)error.Error.Code); + writer.WriteString(Utf8Strings.message, error.Error.Message); + if (error.Error.Data is not null) + { + writer.WritePropertyName(Utf8Strings.data); + WriteUserData(error.Error.Data); + } + + writer.WriteEndObject(); } - writer.WriteStartObject(Utf8Strings.error); - writer.WriteNumber(Utf8Strings.code, (int)error.Error.Code); - writer.WriteString(Utf8Strings.message, error.Error.Message); - if (error.Error.Data is not null) + void WriteUserData(object? value) { - writer.WritePropertyName(Utf8Strings.data); - WriteUserData(error.Error.Data); + JsonSerializer.Serialize(writer, value, this.massagedUserDataSerializerOptions); } - - writer.WriteEndObject(); } - - void WriteUserData(object? value) + catch (Exception ex) { - JsonSerializer.Serialize(writer, value, this.massagedUserDataSerializerOptions); + throw new JsonException(Resources.SerializationFailure, ex); } } } From 81cb7b4b2305e70ae3fcaad6d3c1efdc23f12f22 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 21 Apr 2023 16:21:00 -0600 Subject: [PATCH 22/35] Add activity tracing support All tests now pass. --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index ebf868a1..f52043af 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -102,6 +102,8 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding RequestId = ReadRequestId(), Method = methodElement.GetString(), JsonArguments = document.RootElement.TryGetProperty(Utf8Strings.@params, out JsonElement paramsElement) ? paramsElement : null, + TraceParent = document.RootElement.TryGetProperty(Utf8Strings.traceparent, out JsonElement traceParentElement) ? traceParentElement.GetString() : null, + TraceState = document.RootElement.TryGetProperty(Utf8Strings.tracestate, out JsonElement traceStateElement) ? traceStateElement.GetString() : null, }; message = request; } @@ -183,6 +185,16 @@ public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message) WriteId(request.RequestId); writer.WriteString(Utf8Strings.method, request.Method); WriteArguments(request); + if (request.TraceParent is not null) + { + writer.WriteString(Utf8Strings.traceparent, request.TraceParent); + } + + if (request.TraceState is not null) + { + writer.WriteString(Utf8Strings.tracestate, request.TraceState); + } + break; case Protocol.JsonRpcResult result: WriteId(result.RequestId); @@ -329,6 +341,10 @@ private static class Utf8Strings internal static ReadOnlySpan @params => "params"u8; + internal static ReadOnlySpan traceparent => "traceparent"u8; + + internal static ReadOnlySpan tracestate => "tracestate"u8; + internal static ReadOnlySpan result => "result"u8; internal static ReadOnlySpan error => "error"u8; From b43abd68d93b824b9d7a7e103df43bbba212804f Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 21 Apr 2023 16:34:39 -0600 Subject: [PATCH 23/35] Activate the rest of the exotic type tests --- .../AsyncEnumerableJsonTests.cs | 3 - .../AsyncEnumerableMessagePackTests.cs | 3 - .../AsyncEnumerableSystemTextJsonTests.cs | 16 +++++ .../DisposableProxySystemTextJsonTests.cs | 12 ++++ ...DuplexPipeMarshalingSystemTextJsonTests.cs | 16 +++++ .../MarshalableProxySystemTextJsonTests.cs | 12 ++++ .../ObserverMarshalingSystemTextJsonTests.cs | 12 ++++ .../StreamJsonRpc.Tests.csproj | 7 ++ .../TargetObjectEventsSystemTextJsonTests.cs | 69 +++++++++++++++++++ test/StreamJsonRpc.Tests/Usings.cs | 1 + ...WebSocketMessageHandlerMessagePackTests.cs | 5 +- ...SocketMessageHandlerSystemTextJsonTests.cs | 7 ++ 12 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 test/StreamJsonRpc.Tests/AsyncEnumerableSystemTextJsonTests.cs create mode 100644 test/StreamJsonRpc.Tests/DisposableProxySystemTextJsonTests.cs create mode 100644 test/StreamJsonRpc.Tests/DuplexPipeMarshalingSystemTextJsonTests.cs create mode 100644 test/StreamJsonRpc.Tests/MarshalableProxySystemTextJsonTests.cs create mode 100644 test/StreamJsonRpc.Tests/ObserverMarshalingSystemTextJsonTests.cs create mode 100644 test/StreamJsonRpc.Tests/TargetObjectEventsSystemTextJsonTests.cs create mode 100644 test/StreamJsonRpc.Tests/WebSocketMessageHandlerSystemTextJsonTests.cs diff --git a/test/StreamJsonRpc.Tests/AsyncEnumerableJsonTests.cs b/test/StreamJsonRpc.Tests/AsyncEnumerableJsonTests.cs index e18f47b1..e6b53d6d 100644 --- a/test/StreamJsonRpc.Tests/AsyncEnumerableJsonTests.cs +++ b/test/StreamJsonRpc.Tests/AsyncEnumerableJsonTests.cs @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using StreamJsonRpc; -using Xunit.Abstractions; - public class AsyncEnumerableJsonTests : AsyncEnumerableTests { public AsyncEnumerableJsonTests(ITestOutputHelper logger) diff --git a/test/StreamJsonRpc.Tests/AsyncEnumerableMessagePackTests.cs b/test/StreamJsonRpc.Tests/AsyncEnumerableMessagePackTests.cs index 21bc7b74..613b7d45 100644 --- a/test/StreamJsonRpc.Tests/AsyncEnumerableMessagePackTests.cs +++ b/test/StreamJsonRpc.Tests/AsyncEnumerableMessagePackTests.cs @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using StreamJsonRpc; -using Xunit.Abstractions; - public class AsyncEnumerableMessagePackTests : AsyncEnumerableTests { public AsyncEnumerableMessagePackTests(ITestOutputHelper logger) diff --git a/test/StreamJsonRpc.Tests/AsyncEnumerableSystemTextJsonTests.cs b/test/StreamJsonRpc.Tests/AsyncEnumerableSystemTextJsonTests.cs new file mode 100644 index 00000000..9a3a50aa --- /dev/null +++ b/test/StreamJsonRpc.Tests/AsyncEnumerableSystemTextJsonTests.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +public class AsyncEnumerableSystemTextJsonTests : AsyncEnumerableTests +{ + public AsyncEnumerableSystemTextJsonTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override void InitializeFormattersAndHandlers() + { + this.serverMessageFormatter = new SystemTextJsonFormatter(); + this.clientMessageFormatter = new SystemTextJsonFormatter(); + } +} diff --git a/test/StreamJsonRpc.Tests/DisposableProxySystemTextJsonTests.cs b/test/StreamJsonRpc.Tests/DisposableProxySystemTextJsonTests.cs new file mode 100644 index 00000000..aad5ca93 --- /dev/null +++ b/test/StreamJsonRpc.Tests/DisposableProxySystemTextJsonTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +public class DisposableProxySystemTextJsonTests : DisposableProxyTests +{ + public DisposableProxySystemTextJsonTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override IJsonRpcMessageFormatter CreateFormatter() => new SystemTextJsonFormatter(); +} diff --git a/test/StreamJsonRpc.Tests/DuplexPipeMarshalingSystemTextJsonTests.cs b/test/StreamJsonRpc.Tests/DuplexPipeMarshalingSystemTextJsonTests.cs new file mode 100644 index 00000000..966c5d4d --- /dev/null +++ b/test/StreamJsonRpc.Tests/DuplexPipeMarshalingSystemTextJsonTests.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +public class DuplexPipeMarshalingSystemTextJsonTests : DuplexPipeMarshalingTests +{ + public DuplexPipeMarshalingSystemTextJsonTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override void InitializeFormattersAndHandlers() + { + this.serverMessageFormatter = new SystemTextJsonFormatter { MultiplexingStream = this.serverMx }; + this.clientMessageFormatter = new SystemTextJsonFormatter { MultiplexingStream = this.clientMx }; + } +} diff --git a/test/StreamJsonRpc.Tests/MarshalableProxySystemTextJsonTests.cs b/test/StreamJsonRpc.Tests/MarshalableProxySystemTextJsonTests.cs new file mode 100644 index 00000000..0e874aa4 --- /dev/null +++ b/test/StreamJsonRpc.Tests/MarshalableProxySystemTextJsonTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +public class MarshalableProxySystemTextJsonTests : MarshalableProxyTests +{ + public MarshalableProxySystemTextJsonTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override IJsonRpcMessageFormatter CreateFormatter() => new SystemTextJsonFormatter(); +} diff --git a/test/StreamJsonRpc.Tests/ObserverMarshalingSystemTextJsonTests.cs b/test/StreamJsonRpc.Tests/ObserverMarshalingSystemTextJsonTests.cs new file mode 100644 index 00000000..618430ba --- /dev/null +++ b/test/StreamJsonRpc.Tests/ObserverMarshalingSystemTextJsonTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +public class ObserverMarshalingSystemTextJsonTests : ObserverMarshalingTests +{ + public ObserverMarshalingSystemTextJsonTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override IJsonRpcMessageFormatter CreateFormatter() => new SystemTextJsonFormatter(); +} diff --git a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj index 9097dd0e..f77c8ccd 100644 --- a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj +++ b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj @@ -8,21 +8,28 @@ + + + + + + + diff --git a/test/StreamJsonRpc.Tests/TargetObjectEventsSystemTextJsonTests.cs b/test/StreamJsonRpc.Tests/TargetObjectEventsSystemTextJsonTests.cs new file mode 100644 index 00000000..27d03b0e --- /dev/null +++ b/test/StreamJsonRpc.Tests/TargetObjectEventsSystemTextJsonTests.cs @@ -0,0 +1,69 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft; + +public class TargetObjectEventsSystemTextJsonTests : TargetObjectEventsTests +{ + public TargetObjectEventsSystemTextJsonTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override void InitializeFormattersAndHandlers() + { + var clientFormatter = new SystemTextJsonFormatter + { + JsonSerializerOptions = + { + Converters = + { + new IFruitConverter(), + }, + }, + }; + + var serverFormatter = new SystemTextJsonFormatter + { + JsonSerializerOptions = + { + Converters = + { + new IFruitConverter(), + }, + }, + }; + + this.serverMessageHandler = new HeaderDelimitedMessageHandler(this.serverStream, serverFormatter); + this.clientMessageHandler = new HeaderDelimitedMessageHandler(this.clientStream, clientFormatter); + } + + private class IFruitConverter : JsonConverter + { + public override IFruit? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + Assumes.True(reader.Read()); + Assumes.True(reader.GetString() == nameof(IFruit.Name)); + Assumes.True(reader.Read()); + string? name = reader.GetString(); + return new Fruit(name ?? throw new JsonException("Unexpected null.")); + } + + public override void Write(Utf8JsonWriter writer, IFruit? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartObject(); + writer.WriteString(nameof(IFruit.Name), value.Name); + writer.WriteEndObject(); + } + } +} diff --git a/test/StreamJsonRpc.Tests/Usings.cs b/test/StreamJsonRpc.Tests/Usings.cs index d83575da..b6aeb0a2 100644 --- a/test/StreamJsonRpc.Tests/Usings.cs +++ b/test/StreamJsonRpc.Tests/Usings.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +global using Microsoft; global using StreamJsonRpc; global using StreamJsonRpc.Protocol; global using Xunit; diff --git a/test/StreamJsonRpc.Tests/WebSocketMessageHandlerMessagePackTests.cs b/test/StreamJsonRpc.Tests/WebSocketMessageHandlerMessagePackTests.cs index 07e33bcf..3cc44fd4 100644 --- a/test/StreamJsonRpc.Tests/WebSocketMessageHandlerMessagePackTests.cs +++ b/test/StreamJsonRpc.Tests/WebSocketMessageHandlerMessagePackTests.cs @@ -1,7 +1,4 @@ -using StreamJsonRpc; -using Xunit.Abstractions; - -public class WebSocketMessageHandlerMessagePackTests : WebSocketMessageHandlerTests +public class WebSocketMessageHandlerMessagePackTests : WebSocketMessageHandlerTests { public WebSocketMessageHandlerMessagePackTests(ITestOutputHelper logger) : base(new MessagePackFormatter(), logger) diff --git a/test/StreamJsonRpc.Tests/WebSocketMessageHandlerSystemTextJsonTests.cs b/test/StreamJsonRpc.Tests/WebSocketMessageHandlerSystemTextJsonTests.cs new file mode 100644 index 00000000..80eaee10 --- /dev/null +++ b/test/StreamJsonRpc.Tests/WebSocketMessageHandlerSystemTextJsonTests.cs @@ -0,0 +1,7 @@ +public class WebSocketMessageHandlerSystemTextJsonTests : WebSocketMessageHandlerTests +{ + public WebSocketMessageHandlerSystemTextJsonTests(ITestOutputHelper logger) + : base(new SystemTextJsonFormatter(), logger) + { + } +} From db84b576eb957601bb156e23bd2c33fe0639231b Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 24 Apr 2023 12:45:24 -0600 Subject: [PATCH 24/35] Add support for OOB stream exotic types --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 132 ++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index f52043af..27221d68 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO.Pipelines; using System.Reflection; using System.Runtime.ExceptionServices; using System.Runtime.Serialization; @@ -13,6 +14,7 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using Nerdbank.Streams; using StreamJsonRpc.Protocol; using StreamJsonRpc.Reflection; @@ -321,6 +323,10 @@ private JsonSerializerOptions MassageUserDataSerializerOptions(JsonSerializerOpt // Add support for exotic types. options.Converters.Add(new ProgressConverterFactory(this)); + options.Converters.Add(new DuplexPipeConverter(this)); + options.Converters.Add(new PipeReaderConverter(this)); + options.Converters.Add(new PipeWriterConverter(this)); + options.Converters.Add(new StreamConverter(this)); // Add support for serializing exceptions. options.Converters.Add(new ExceptionConverter(this)); @@ -769,6 +775,130 @@ public override void Write(Utf8JsonWriter writer, IProgress? value, JsonSeria } } + private class DuplexPipeConverter : JsonConverter + { + private readonly SystemTextJsonFormatter formatter; + + internal DuplexPipeConverter(SystemTextJsonFormatter formatter) + { + this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + } + + public override bool CanConvert(Type typeToConvert) => typeof(IDuplexPipe).IsAssignableFrom(typeToConvert); + + public override IDuplexPipe? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType == JsonTokenType.Null + ? null + : this.formatter.DuplexPipeTracker!.GetPipe(reader.GetUInt64()); + } + + public override void Write(Utf8JsonWriter writer, IDuplexPipe? value, JsonSerializerOptions options) + { + if (this.formatter.DuplexPipeTracker.GetULongToken(value) is ulong token) + { + writer.WriteNumberValue(token); + } + else + { + writer.WriteNullValue(); + } + } + } + + private class PipeReaderConverter : JsonConverter + { + private readonly SystemTextJsonFormatter formatter; + + internal PipeReaderConverter(SystemTextJsonFormatter formatter) + { + this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + } + + public override bool CanConvert(Type typeToConvert) => typeof(PipeReader).IsAssignableFrom(typeToConvert); + + public override PipeReader? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType == JsonTokenType.Null + ? null + : this.formatter.DuplexPipeTracker!.GetPipeReader(reader.GetUInt64()); + } + + public override void Write(Utf8JsonWriter writer, PipeReader? value, JsonSerializerOptions options) + { + if (this.formatter.DuplexPipeTracker.GetULongToken(value) is ulong token) + { + writer.WriteNumberValue(token); + } + else + { + writer.WriteNullValue(); + } + } + } + + private class PipeWriterConverter : JsonConverter + { + private readonly SystemTextJsonFormatter formatter; + + internal PipeWriterConverter(SystemTextJsonFormatter formatter) + { + this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + } + + public override bool CanConvert(Type typeToConvert) => typeof(PipeWriter).IsAssignableFrom(typeToConvert); + + public override PipeWriter? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType == JsonTokenType.Null + ? null + : this.formatter.DuplexPipeTracker!.GetPipeWriter(reader.GetUInt64()); + } + + public override void Write(Utf8JsonWriter writer, PipeWriter? value, JsonSerializerOptions options) + { + if (this.formatter.DuplexPipeTracker.GetULongToken(value) is ulong token) + { + writer.WriteNumberValue(token); + } + else + { + writer.WriteNullValue(); + } + } + } + + private class StreamConverter : JsonConverter + { + private readonly SystemTextJsonFormatter formatter; + + internal StreamConverter(SystemTextJsonFormatter formatter) + { + this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + } + + public override bool CanConvert(Type typeToConvert) => typeof(Stream).IsAssignableFrom(typeToConvert); + + public override Stream? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType == JsonTokenType.Null + ? null + : this.formatter.DuplexPipeTracker!.GetPipe(reader.GetUInt64()).AsStream(); + } + + public override void Write(Utf8JsonWriter writer, Stream? value, JsonSerializerOptions options) + { + if (this.formatter.DuplexPipeTracker.GetULongToken(value?.UsePipe()) is ulong token) + { + writer.WriteNumberValue(token); + } + else + { + writer.WriteNullValue(); + } + } + } + private class ExceptionConverter : JsonConverter { /// @@ -778,7 +908,7 @@ private class ExceptionConverter : JsonConverter private readonly SystemTextJsonFormatter formatter; - public ExceptionConverter(SystemTextJsonFormatter formatter) + internal ExceptionConverter(SystemTextJsonFormatter formatter) { this.formatter = formatter; } From 955f837801b1a60da1d3615eb58e5d68bc055db0 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 24 Apr 2023 13:13:46 -0600 Subject: [PATCH 25/35] Add `IAsyncEnumerable` support --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 89 +++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 27221d68..80420b4a 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -323,6 +323,7 @@ private JsonSerializerOptions MassageUserDataSerializerOptions(JsonSerializerOpt // Add support for exotic types. options.Converters.Add(new ProgressConverterFactory(this)); + options.Converters.Add(new AsyncEnumerableConverter(this)); options.Converters.Add(new DuplexPipeConverter(this)); options.Converters.Add(new PipeReaderConverter(this)); options.Converters.Add(new PipeWriterConverter(this)); @@ -730,12 +731,14 @@ internal ProgressConverterFactory(SystemTextJsonFormatter formatter) this.formatter = formatter; } - public override bool CanConvert(Type typeToConvert) => MessageFormatterProgressTracker.CanSerialize(typeToConvert) || MessageFormatterProgressTracker.CanDeserialize(typeToConvert); + public override bool CanConvert(Type typeToConvert) => TrackerHelpers>.FindInterfaceImplementedBy(typeToConvert) is not null; public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - Type progressType = typeToConvert.GetGenericArguments()[0]; - Type converterType = typeof(Converter<>).MakeGenericType(progressType); + Type? iface = TrackerHelpers>.FindInterfaceImplementedBy(typeToConvert); + Assumes.NotNull(iface); + Type genericTypeArg = iface.GetGenericArguments()[0]; + Type converterType = typeof(Converter<>).MakeGenericType(genericTypeArg); return (JsonConverter)Activator.CreateInstance(converterType, this.formatter)!; } @@ -775,6 +778,86 @@ public override void Write(Utf8JsonWriter writer, IProgress? value, JsonSeria } } + private class AsyncEnumerableConverter : JsonConverterFactory + { + private readonly SystemTextJsonFormatter formatter; + + internal AsyncEnumerableConverter(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override bool CanConvert(Type typeToConvert) => TrackerHelpers>.FindInterfaceImplementedBy(typeToConvert) is not null; + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type? iface = TrackerHelpers>.FindInterfaceImplementedBy(typeToConvert); + Assumes.NotNull(iface); + Type genericTypeArg = iface.GetGenericArguments()[0]; + Type converterType = typeof(Converter<>).MakeGenericType(genericTypeArg); + return (JsonConverter)Activator.CreateInstance(converterType, this.formatter)!; + } + + private class Converter : JsonConverter?> + { + private readonly SystemTextJsonFormatter formatter; + + public Converter(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override IAsyncEnumerable? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + using JsonDocument wrapper = JsonDocument.ParseValue(ref reader); + JsonElement? handle = null; + if (wrapper.RootElement.TryGetProperty(MessageFormatterEnumerableTracker.TokenPropertyName, out JsonElement enumToken)) + { + // Copy the token so we can retain it and replay it later. + handle = enumToken.Deserialize(); + } + + IReadOnlyList? prefetchedItems = null; + if (wrapper.RootElement.TryGetProperty(MessageFormatterEnumerableTracker.ValuesPropertyName, out JsonElement prefetchedElement)) + { + prefetchedItems = prefetchedElement.Deserialize>(options); + } + + return this.formatter.EnumerableTracker.CreateEnumerableProxy(handle, prefetchedItems); + } + + public override void Write(Utf8JsonWriter writer, IAsyncEnumerable? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + (IReadOnlyList Elements, bool Finished) prefetched = value.TearOffPrefetchedElements(); + long token = this.formatter.EnumerableTracker.GetToken(value); + writer.WriteStartObject(); + if (!prefetched.Finished) + { + writer.WriteNumber(MessageFormatterEnumerableTracker.TokenPropertyName, token); + } + + if (prefetched.Elements.Count > 0) + { + writer.WritePropertyName(MessageFormatterEnumerableTracker.ValuesPropertyName); + JsonSerializer.Serialize(writer, prefetched.Elements, options); + } + + writer.WriteEndObject(); + } + } + } + private class DuplexPipeConverter : JsonConverter { private readonly SystemTextJsonFormatter formatter; From f4960aed092beb28643b84c46073ffb179833355 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 24 Apr 2023 18:33:21 -0600 Subject: [PATCH 26/35] Add support for RPC general marshable objects --- ...sageFormatterRpcMarshaledContextTracker.cs | 8 +- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 85 +++++++++++++++++-- 2 files changed, 82 insertions(+), 11 deletions(-) diff --git a/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs b/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs index 7d8418fd..4940fa53 100644 --- a/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs +++ b/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs @@ -391,16 +391,16 @@ public MarshalToken(int __jsonrpc_marshaled, long handle, string? lifetime, int[ } [DataMember(Name = "__jsonrpc_marshaled", IsRequired = true)] - public int Marshaled { get; } + public int Marshaled { get; set; } [DataMember(Name = "handle", IsRequired = true)] - public long Handle { get; } + public long Handle { get; set; } [DataMember(Name = "lifetime", EmitDefaultValue = false)] - public string? Lifetime { get; } + public string? Lifetime { get; set; } [DataMember(Name = "optionalInterfaces", EmitDefaultValue = false)] - public int[]? OptionalInterfacesCodes { get; } + public int[]? OptionalInterfacesCodes { get; set; } } /// diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 80420b4a..bf925c3f 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -249,7 +249,7 @@ void WriteArguments(Protocol.JsonRpcRequest request) writer.WriteStartArray(Utf8Strings.@params); for (int i = 0; i < request.ArgumentsList.Count; i++) { - WriteUserData(request.ArgumentsList[i]); + WriteUserData(request.ArgumentsList[i], request.ArgumentListDeclaredTypes?[i]); } writer.WriteEndArray(); @@ -260,7 +260,7 @@ void WriteArguments(Protocol.JsonRpcRequest request) foreach (KeyValuePair argument in request.NamedArguments) { writer.WritePropertyName(argument.Key); - WriteUserData(argument.Value); + WriteUserData(argument.Value, request.NamedArgumentDeclaredTypes?[argument.Key]); } writer.WriteEndObject(); @@ -269,14 +269,14 @@ void WriteArguments(Protocol.JsonRpcRequest request) { // This is a custom named arguments object, so we'll just serialize it as-is. writer.WritePropertyName(Utf8Strings.@params); - WriteUserData(request.Arguments); + WriteUserData(request.Arguments, declaredType: null); } } void WriteResult(Protocol.JsonRpcResult result) { writer.WritePropertyName(Utf8Strings.result); - WriteUserData(result.Result); + WriteUserData(result.Result, result.ResultDeclaredType); } void WriteError(Protocol.JsonRpcError error) @@ -292,15 +292,22 @@ void WriteError(Protocol.JsonRpcError error) if (error.Error.Data is not null) { writer.WritePropertyName(Utf8Strings.data); - WriteUserData(error.Error.Data); + WriteUserData(error.Error.Data, null); } writer.WriteEndObject(); } - void WriteUserData(object? value) + void WriteUserData(object? value, Type? declaredType) { - JsonSerializer.Serialize(writer, value, this.massagedUserDataSerializerOptions); + if (declaredType is not null) + { + JsonSerializer.Serialize(writer, value, declaredType, this.massagedUserDataSerializerOptions); + } + else + { + JsonSerializer.Serialize(writer, value, this.massagedUserDataSerializerOptions); + } } } catch (Exception ex) @@ -324,6 +331,7 @@ private JsonSerializerOptions MassageUserDataSerializerOptions(JsonSerializerOpt // Add support for exotic types. options.Converters.Add(new ProgressConverterFactory(this)); options.Converters.Add(new AsyncEnumerableConverter(this)); + options.Converters.Add(new RpcMarshalableConverterFactory(this)); options.Converters.Add(new DuplexPipeConverter(this)); options.Converters.Add(new PipeReaderConverter(this)); options.Converters.Add(new PipeWriterConverter(this)); @@ -858,6 +866,69 @@ public override void Write(Utf8JsonWriter writer, IAsyncEnumerable? value, Js } } + private class RpcMarshalableConverterFactory : JsonConverterFactory + { + private readonly SystemTextJsonFormatter formatter; + + public RpcMarshalableConverterFactory(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override bool CanConvert(Type typeToConvert) + { + return MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(typeToConvert, out _, out _); + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Assumes.True(MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(typeToConvert, out JsonRpcProxyOptions? proxyOptions, out JsonRpcTargetOptions? targetOptions)); + return (JsonConverter)Activator.CreateInstance( + typeof(Converter<>).MakeGenericType(typeToConvert), + this.formatter, + proxyOptions, + targetOptions)!; + } + + private class Converter : JsonConverter + where T : class + { + private readonly SystemTextJsonFormatter formatter; + private readonly JsonRpcProxyOptions proxyOptions; + private readonly JsonRpcTargetOptions targetOptions; + + public Converter(SystemTextJsonFormatter formatter, JsonRpcProxyOptions proxyOptions, JsonRpcTargetOptions targetOptions) + { + this.formatter = formatter; + this.proxyOptions = proxyOptions; + this.targetOptions = targetOptions; + } + + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + MessageFormatterRpcMarshaledContextTracker.MarshalToken token = JsonSerializer.Deserialize(ref reader, options); + return (T?)this.formatter.RpcMarshaledContextTracker.GetObject(typeof(T), token, this.proxyOptions); + } + + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + MessageFormatterRpcMarshaledContextTracker.MarshalToken token = this.formatter.RpcMarshaledContextTracker.GetToken(value, this.targetOptions, typeof(T)); + JsonSerializer.Serialize(writer, token, options); + } + } + } + private class DuplexPipeConverter : JsonConverter { private readonly SystemTextJsonFormatter formatter; From ab26715a4424be5b662a25d01e23f1b80f01a480 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 25 Apr 2023 14:00:04 -0600 Subject: [PATCH 27/35] Fix regression due to declared types --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index bf925c3f..c9411932 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -300,7 +300,7 @@ void WriteError(Protocol.JsonRpcError error) void WriteUserData(object? value, Type? declaredType) { - if (declaredType is not null) + if (declaredType is not null && value is not null) { JsonSerializer.Serialize(writer, value, declaredType, this.massagedUserDataSerializerOptions); } From dee7465c33d03c2d03cbe7d790ee5904dc8c2ed0 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 25 Apr 2023 16:34:58 -0600 Subject: [PATCH 28/35] Fix IFruitConverter test --- .../TargetObjectEventsSystemTextJsonTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/StreamJsonRpc.Tests/TargetObjectEventsSystemTextJsonTests.cs b/test/StreamJsonRpc.Tests/TargetObjectEventsSystemTextJsonTests.cs index 27d03b0e..e98fde4f 100644 --- a/test/StreamJsonRpc.Tests/TargetObjectEventsSystemTextJsonTests.cs +++ b/test/StreamJsonRpc.Tests/TargetObjectEventsSystemTextJsonTests.cs @@ -46,10 +46,18 @@ private class IFruitConverter : JsonConverter return null; } + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + Assumes.True(reader.Read()); Assumes.True(reader.GetString() == nameof(IFruit.Name)); + Assumes.True(reader.Read()); string? name = reader.GetString(); + + // Read to the end object token. + reader.Read(); + Assert.Equal(JsonTokenType.EndObject, reader.TokenType); + return new Fruit(name ?? throw new JsonException("Unexpected null.")); } From 3b3ddc66ad44701e39dc7bfa2efd9666f4d59c20 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 25 Apr 2023 16:42:40 -0600 Subject: [PATCH 29/35] Fix concurrency bug --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index c9411932..9ce760b5 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Buffers; +using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -1253,7 +1254,7 @@ public object Convert(object value, TypeCode typeCode) private class DataContractResolver : IJsonTypeInfoResolver { - private readonly Dictionary typeInfoCache = new(); + private readonly ConcurrentDictionary typeInfoCache = new(); private readonly bool onlyRecognizeDecoratedTypes; @@ -1281,7 +1282,7 @@ internal DataContractResolver(bool onlyRecognizeDecoratedTypes) typeInfo = this.fallbackResolver.GetTypeInfo(type, options); } - this.typeInfoCache.Add(type, typeInfo); + this.typeInfoCache.TryAdd(type, typeInfo); } return typeInfo; From e11290c62cd117be0275c78b58e14518ef89ff21 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 26 Apr 2023 10:45:40 -0600 Subject: [PATCH 30/35] Fix the last of the failing tests --- test/StreamJsonRpc.Tests/DisposableProxyJsonTests.cs | 5 +++-- .../DisposableProxyMessagePackTests.cs | 5 +++-- .../DisposableProxySystemTextJsonTests.cs | 4 ++++ test/StreamJsonRpc.Tests/DisposableProxyTests.cs | 10 +++------- test/StreamJsonRpc.Tests/MarshalableProxyJsonTests.cs | 5 +++-- .../MarshalableProxyMessagePackTests.cs | 5 +++-- .../MarshalableProxySystemTextJsonTests.cs | 4 ++++ test/StreamJsonRpc.Tests/MarshalableProxyTests.cs | 5 ++++- 8 files changed, 27 insertions(+), 16 deletions(-) diff --git a/test/StreamJsonRpc.Tests/DisposableProxyJsonTests.cs b/test/StreamJsonRpc.Tests/DisposableProxyJsonTests.cs index b819dfec..25fde941 100644 --- a/test/StreamJsonRpc.Tests/DisposableProxyJsonTests.cs +++ b/test/StreamJsonRpc.Tests/DisposableProxyJsonTests.cs @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using StreamJsonRpc; -using Xunit.Abstractions; +using Newtonsoft.Json; public class DisposableProxyJsonTests : DisposableProxyTests { @@ -11,5 +10,7 @@ public DisposableProxyJsonTests(ITestOutputHelper logger) { } + protected override Type FormatterExceptionType => typeof(JsonSerializationException); + protected override IJsonRpcMessageFormatter CreateFormatter() => new JsonMessageFormatter(); } diff --git a/test/StreamJsonRpc.Tests/DisposableProxyMessagePackTests.cs b/test/StreamJsonRpc.Tests/DisposableProxyMessagePackTests.cs index 5cf7cd94..38c4f2ef 100644 --- a/test/StreamJsonRpc.Tests/DisposableProxyMessagePackTests.cs +++ b/test/StreamJsonRpc.Tests/DisposableProxyMessagePackTests.cs @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using StreamJsonRpc; -using Xunit.Abstractions; +using MessagePack; public class DisposableProxyMessagePackTests : DisposableProxyTests { @@ -11,5 +10,7 @@ public DisposableProxyMessagePackTests(ITestOutputHelper logger) { } + protected override Type FormatterExceptionType => typeof(MessagePackSerializationException); + protected override IJsonRpcMessageFormatter CreateFormatter() => new MessagePackFormatter(); } diff --git a/test/StreamJsonRpc.Tests/DisposableProxySystemTextJsonTests.cs b/test/StreamJsonRpc.Tests/DisposableProxySystemTextJsonTests.cs index aad5ca93..86eeb1e6 100644 --- a/test/StreamJsonRpc.Tests/DisposableProxySystemTextJsonTests.cs +++ b/test/StreamJsonRpc.Tests/DisposableProxySystemTextJsonTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Text.Json; + public class DisposableProxySystemTextJsonTests : DisposableProxyTests { public DisposableProxySystemTextJsonTests(ITestOutputHelper logger) @@ -8,5 +10,7 @@ public DisposableProxySystemTextJsonTests(ITestOutputHelper logger) { } + protected override Type FormatterExceptionType => typeof(JsonException); + protected override IJsonRpcMessageFormatter CreateFormatter() => new SystemTextJsonFormatter(); } diff --git a/test/StreamJsonRpc.Tests/DisposableProxyTests.cs b/test/StreamJsonRpc.Tests/DisposableProxyTests.cs index 778afe97..2d1b412b 100644 --- a/test/StreamJsonRpc.Tests/DisposableProxyTests.cs +++ b/test/StreamJsonRpc.Tests/DisposableProxyTests.cs @@ -4,14 +4,8 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.Serialization; -using MessagePack; -using Microsoft; using Microsoft.VisualStudio.Threading; using Nerdbank.Streams; -using Newtonsoft.Json; -using StreamJsonRpc; -using Xunit; -using Xunit.Abstractions; #pragma warning disable CA2214 // Do not call virtual methods in constructors @@ -60,6 +54,8 @@ public interface IServer Task AcceptDataContainerAsync(DataContainer dataContainer); } + protected abstract Type FormatterExceptionType { get; } + [Fact] public async Task NoLeakWhenTransmissionFailsAfterTokenGenerated() { @@ -208,7 +204,7 @@ private async Task NoLeakWhenTransmissionFailsAfterTokenGenerated new object?[] { disposable, new JsonRpcTests.TypeThrowsWhenSerialized() }, new Type[] { typeof(IDisposable), typeof(JsonRpcTests.TypeThrowsWhenSerialized) }, this.TimeoutToken)); - Assert.True(ex is JsonSerializationException || ex is MessagePackSerializationException); + Assert.IsAssignableFrom(this.FormatterExceptionType, ex); Assert.True(IsExceptionOrInnerOfType(ex, exactTypeMatch: true)); return new WeakReference(disposable); diff --git a/test/StreamJsonRpc.Tests/MarshalableProxyJsonTests.cs b/test/StreamJsonRpc.Tests/MarshalableProxyJsonTests.cs index 63697dcd..6195dd40 100644 --- a/test/StreamJsonRpc.Tests/MarshalableProxyJsonTests.cs +++ b/test/StreamJsonRpc.Tests/MarshalableProxyJsonTests.cs @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using StreamJsonRpc; -using Xunit.Abstractions; +using Newtonsoft.Json; public class MarshalableProxyJsonTests : MarshalableProxyTests { @@ -11,5 +10,7 @@ public MarshalableProxyJsonTests(ITestOutputHelper logger) { } + protected override Type FormatterExceptionType => typeof(JsonSerializationException); + protected override IJsonRpcMessageFormatter CreateFormatter() => new JsonMessageFormatter(); } diff --git a/test/StreamJsonRpc.Tests/MarshalableProxyMessagePackTests.cs b/test/StreamJsonRpc.Tests/MarshalableProxyMessagePackTests.cs index 5415620c..4db0aecf 100644 --- a/test/StreamJsonRpc.Tests/MarshalableProxyMessagePackTests.cs +++ b/test/StreamJsonRpc.Tests/MarshalableProxyMessagePackTests.cs @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using StreamJsonRpc; -using Xunit.Abstractions; +using MessagePack; public class MarshalableProxyMessagePackTests : MarshalableProxyTests { @@ -11,5 +10,7 @@ public MarshalableProxyMessagePackTests(ITestOutputHelper logger) { } + protected override Type FormatterExceptionType => typeof(MessagePackSerializationException); + protected override IJsonRpcMessageFormatter CreateFormatter() => new MessagePackFormatter(); } diff --git a/test/StreamJsonRpc.Tests/MarshalableProxySystemTextJsonTests.cs b/test/StreamJsonRpc.Tests/MarshalableProxySystemTextJsonTests.cs index 0e874aa4..4a57942b 100644 --- a/test/StreamJsonRpc.Tests/MarshalableProxySystemTextJsonTests.cs +++ b/test/StreamJsonRpc.Tests/MarshalableProxySystemTextJsonTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Text.Json; + public class MarshalableProxySystemTextJsonTests : MarshalableProxyTests { public MarshalableProxySystemTextJsonTests(ITestOutputHelper logger) @@ -8,5 +10,7 @@ public MarshalableProxySystemTextJsonTests(ITestOutputHelper logger) { } + protected override Type FormatterExceptionType => typeof(JsonException); + protected override IJsonRpcMessageFormatter CreateFormatter() => new SystemTextJsonFormatter(); } diff --git a/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs b/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs index 7bee74e0..378e6b67 100644 --- a/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs +++ b/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.Serialization; @@ -265,6 +266,8 @@ public interface IServer Task AcceptNonMarshalableDerivedFromMarshalablesAsync(INonMarshalableDerivedFromMarshalable nonMarshalable); } + protected abstract Type FormatterExceptionType { get; } + [Fact] public async Task NoLeakWhenTransmissionFailsAfterTokenGenerated() { @@ -280,7 +283,7 @@ async Task Helper() new object?[] { marshalable, new JsonRpcTests.TypeThrowsWhenSerialized() }, new Type[] { typeof(IMarshalable), typeof(JsonRpcTests.TypeThrowsWhenSerialized) }, this.TimeoutToken)); - Assert.True(ex is JsonSerializationException || ex is MessagePackSerializationException); + Assert.IsAssignableFrom(this.FormatterExceptionType, ex); Assert.True(IsExceptionOrInnerOfType(ex, exactTypeMatch: true)); return new WeakReference(marshalable); From 615fc2b9a43810f966ef1272cdcd57688a4209cf Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 26 Apr 2023 11:06:36 -0600 Subject: [PATCH 31/35] Document our support of System.Text.Json --- doc/extensibility.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/doc/extensibility.md b/doc/extensibility.md index 3176b9d5..c7f824e4 100644 --- a/doc/extensibility.md +++ b/doc/extensibility.md @@ -88,14 +88,24 @@ StreamJsonRpc includes the following `IJsonRpcMessageFormatter` implementations: You can contribute your own via `MessagePackFormatter.SetOptions(MessagePackSerializationOptions)`. See alternative formatters below. +1. `SystemTextJsonFormatter` - Uses the [`System.Text.Json` library][SystemTextJson] to serialize each + JSON-RPC message as UTF-8 encoded JSON. + All RPC method parameters and return types must be serializable by System.Text.Json, + with the additional benefit of `DataContract` and `DataMember` attributes being supported by default + within StreamJsonRpc where System.Text.Json alone does not support them. + You can leverage `JsonConverter` and add your custom converters via attributes or by + contributing them to the `SystemTextJsonFormatter.JsonSerializerOptions.Converters` collection. + When authoring a custom `IJsonRpcMessageFormatter` implementation, consider supporting the [exotic types](exotic_types.md) that require formatter participation. We have helper classes to make this straightforward. Refer to the source code from our built-in formatters to see how to use these helper classes. -### Alternative formatters +### Choosing your formatter + +#### When to use `MessagePackFormatter` -For performance reasons when both parties can agree, it may be appropriate to switch out the textual JSON - representation for something that can be serialized faster and/or in a more compact format. +The very best performance comes from using the `MessagePackFormatter` with the `LengthHeaderMessageHandler`. +This combination is the fastest and produces the most compact serialized format. The [MessagePack format][MessagePackFormat] is a fast, binary serialization format that resembles the structure of JSON. It can be used as a substitute for JSON when both parties agree on the protocol for @@ -104,7 +114,18 @@ significant wins in terms of performance and payload size. Utilizing `MessagePack` for exchanging JSON-RPC messages is incredibly easy. Check out the `BasicJsonRpc` method in our [MessagePackFormatterTests][MessagePackUsage] class. +#### When to use `SystemTextJsonFormatter` + +When the remote party does not support MessagePack but does support UTF-8 encoded JSON, +`SystemTextJsonFormatter` offers the most performant choice available. + +#### When to use `JsonMessageFormatter` + +This formatter is the default for legacy reasons, and offers compatibility with data types that can only be serialized with Newtonsoft.Json. +It produces JSON text and allows configuring the text encoding, with UTF-8 being the default. + [MessagePackLibrary]: https://github.com/neuecc/MessagePack-CSharp [MessagePackUsage]: ../src/StreamJsonRpc.Tests/MessagePackFormatterTests.cs [MessagePackFormat]: https://msgpack.org/ +[SystemTextJson]: https://learn.microsoft.com/dotnet/standard/serialization/system-text-json/overview [spec]: https://www.jsonrpc.org/specification From 628591eaa43968fcd2c21045308c6470b649f358 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 26 Apr 2023 11:28:49 -0600 Subject: [PATCH 32/35] Add support for tracing messages --- src/StreamJsonRpc/MessagePackFormatter.cs | 2 +- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 58 ++++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/StreamJsonRpc/MessagePackFormatter.cs b/src/StreamJsonRpc/MessagePackFormatter.cs index 30b7ac80..8b43c0eb 100644 --- a/src/StreamJsonRpc/MessagePackFormatter.cs +++ b/src/StreamJsonRpc/MessagePackFormatter.cs @@ -707,7 +707,7 @@ public object Convert(object value, TypeCode typeCode) /// /// /// In perf traces, creation of this object used to show up as one of the most allocated objects. - /// It is used even when tracing isn't active. So we changed its design it to be reused, + /// It is used even when tracing isn't active. So we changed its design to be reused, /// since its lifetime is only required during a synchronous call to a trace API. /// private class ToStringHelper diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 9ce760b5..d632f45f 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -24,7 +24,7 @@ namespace StreamJsonRpc; /// /// A formatter that emits UTF-8 encoded JSON where user data should be serializable via the . /// -public partial class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcFormatterState, IJsonRpcMessageFactory +public partial class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcFormatterState, IJsonRpcMessageFactory, IJsonRpcFormatterTracingCallbacks { private static readonly JsonWriterOptions WriterOptions = new() { }; @@ -46,6 +46,8 @@ public partial class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFor /// private static readonly Encoding DefaultEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + private readonly ToStringHelper serializationToStringHelper = new ToStringHelper(); + private JsonSerializerOptions massagedUserDataSerializerOptions; /// @@ -161,16 +163,16 @@ RequestId ReadRequestId() : RequestId.NotSpecified; } + IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc; + tracingCallbacks?.OnMessageDeserialized(message, document.RootElement); + this.TryHandleSpecialIncomingMessage(message); return message; } /// - public object GetJsonText(JsonRpcMessage message) - { - throw new NotImplementedException(); - } + public object GetJsonText(JsonRpcMessage message) => throw new NotSupportedException(); /// public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message) @@ -318,6 +320,20 @@ void WriteUserData(object? value, Type? declaredType) } } + void IJsonRpcFormatterTracingCallbacks.OnSerializationComplete(JsonRpcMessage message, ReadOnlySequence encodedMessage) + { + IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc; + this.serializationToStringHelper.Activate(encodedMessage); + try + { + tracingCallbacks?.OnMessageSerialized(message, this.serializationToStringHelper); + } + finally + { + this.serializationToStringHelper.Deactivate(); + } + } + Protocol.JsonRpcRequest IJsonRpcMessageFactory.CreateRequestMessage() => new JsonRpcRequest(this); Protocol.JsonRpcError IJsonRpcMessageFactory.CreateErrorMessage() => new JsonRpcError(this); @@ -1352,4 +1368,36 @@ bool TryCreateJsonPropertyInfo(MemberInfo memberInfo, Type propertyType, [NotNul } } } + + /// + private class ToStringHelper + { + private ReadOnlySequence? encodedMessage; + private string? jsonString; + + public override string ToString() + { + Verify.Operation(this.encodedMessage.HasValue, "This object has not been activated. It may have already been recycled."); + + using JsonDocument doc = JsonDocument.Parse(this.encodedMessage.Value); + return this.jsonString ??= doc.RootElement.ToString(); + } + + /// + /// Initializes this object to represent a message. + /// + internal void Activate(ReadOnlySequence encodedMessage) + { + this.encodedMessage = encodedMessage; + } + + /// + /// Cleans out this object to release memory and ensure throws if someone uses it after deactivation. + /// + internal void Deactivate() + { + this.encodedMessage = null; + this.jsonString = null; + } + } } From 51f521533cdd964682c304d9b825c344f785738b Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 26 Apr 2023 11:34:29 -0600 Subject: [PATCH 33/35] Remove unnecessary `partial` modifier on the new clas --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index d632f45f..6839e230 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -24,7 +24,7 @@ namespace StreamJsonRpc; /// /// A formatter that emits UTF-8 encoded JSON where user data should be serializable via the . /// -public partial class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcFormatterState, IJsonRpcMessageFactory, IJsonRpcFormatterTracingCallbacks +public class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcFormatterState, IJsonRpcMessageFactory, IJsonRpcFormatterTracingCallbacks { private static readonly JsonWriterOptions WriterOptions = new() { }; From 961f81fb6fbbfbeb9a3c8f8f0395d011e4433252 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 26 Apr 2023 11:39:39 -0600 Subject: [PATCH 34/35] Recycle more memory --- src/StreamJsonRpc/SystemTextJsonFormatter.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 6839e230..012f57d6 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -50,6 +50,11 @@ public class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, private JsonSerializerOptions massagedUserDataSerializerOptions; + /// + /// Retains the message currently being deserialized so that it can be disposed when we're done with it. + /// + private JsonDocument? deserializingDocument; + /// /// Initializes a new instance of the class. /// @@ -92,8 +97,7 @@ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding throw new NotSupportedException("Only our default encoding is supported."); } - // TODO: dispose of the document when we're done with it. This means each JsonRpcMessage will need to dispose of it in ReleaseBuffers since they capture pieces of it. - JsonDocument document = JsonDocument.Parse(contentBuffer, DocumentOptions); + JsonDocument document = this.deserializingDocument = JsonDocument.Parse(contentBuffer, DocumentOptions); if (document.RootElement.ValueKind != JsonValueKind.Object) { throw new JsonException("Expected a JSON object at the root of the message."); @@ -562,6 +566,8 @@ protected override void ReleaseBuffers() { base.ReleaseBuffers(); this.jsonArguments = null; + this.formatter.deserializingDocument?.Dispose(); + this.formatter.deserializingDocument = null; } private static int CountArguments(JsonElement arguments) @@ -638,6 +644,8 @@ protected override void ReleaseBuffers() { base.ReleaseBuffers(); this.JsonResult = null; + this.formatter.deserializingDocument?.Dispose(); + this.formatter.deserializingDocument = null; } } @@ -665,6 +673,9 @@ protected override void ReleaseBuffers() { detail.JsonData = null; } + + this.formatter.deserializingDocument?.Dispose(); + this.formatter.deserializingDocument = null; } internal new class ErrorDetail : Protocol.JsonRpcError.ErrorDetail From 0f649cc46e5c21fb6add36e033a754ac73137983 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 27 Apr 2023 21:02:41 -0600 Subject: [PATCH 35/35] Applied PR feedback --- .vscode/settings.json | 3 +- doc/extensibility.md | 6 + src/StreamJsonRpc/SystemTextJsonFormatter.cs | 150 +++++------------- .../SystemTextJsonFormatterTests.cs | 54 +++++++ 4 files changed, 98 insertions(+), 115 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2a5afea1..3ae1371c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,5 @@ "files.trimFinalNewlines": true, "omnisharp.enableEditorConfigSupport": true, "omnisharp.enableImportCompletion": true, - "omnisharp.enableRoslynAnalyzers": true, - "dotnet.defaultSolution": "StreamJsonRpc.sln" + "omnisharp.enableRoslynAnalyzers": true } diff --git a/doc/extensibility.md b/doc/extensibility.md index c7f824e4..21005758 100644 --- a/doc/extensibility.md +++ b/doc/extensibility.md @@ -119,11 +119,17 @@ Check out the `BasicJsonRpc` method in our [MessagePackFormatterTests][MessagePa When the remote party does not support MessagePack but does support UTF-8 encoded JSON, `SystemTextJsonFormatter` offers the most performant choice available. +This formatter is compatible with remote systems that use `JsonMessageFormatter`, provided they use the default UTF-8 encoding. +The remote party must also use the same message handler, such as `HeaderDelimitedMessageHandler`. + #### When to use `JsonMessageFormatter` This formatter is the default for legacy reasons, and offers compatibility with data types that can only be serialized with Newtonsoft.Json. It produces JSON text and allows configuring the text encoding, with UTF-8 being the default. +This formatter is compatible with remote systems that use `SystemTextJsonFormatter` when using the default UTF-8 encoding. +The remote party must also use the same message handler, such as `HeaderDelimitedMessageHandler`. + [MessagePackLibrary]: https://github.com/neuecc/MessagePack-CSharp [MessagePackUsage]: ../src/StreamJsonRpc.Tests/MessagePackFormatterTests.cs [MessagePackFormat]: https://msgpack.org/ diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 012f57d6..049396c0 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -572,17 +572,15 @@ protected override void ReleaseBuffers() private static int CountArguments(JsonElement arguments) { - int count = 0; + int count; switch (arguments.ValueKind) { case JsonValueKind.Array: - foreach (JsonElement element in arguments.EnumerateArray()) - { - count++; - } + count = arguments.GetArrayLength(); break; case JsonValueKind.Object: + count = 0; foreach (JsonProperty property in arguments.EnumerateObject()) { count++; @@ -778,7 +776,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer return (JsonConverter)Activator.CreateInstance(converterType, this.formatter)!; } - private class Converter : JsonConverter?> + private class Converter : JsonConverter> { private readonly SystemTextJsonFormatter formatter; @@ -787,13 +785,8 @@ public Converter(SystemTextJsonFormatter formatter) this.formatter = formatter; } - public override IProgress? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override IProgress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - Assumes.NotNull(this.formatter.JsonRpc); object token = reader.TokenType switch { @@ -803,13 +796,12 @@ public Converter(SystemTextJsonFormatter formatter) }; bool clientRequiresNamedArgs = this.formatter.ApplicableMethodAttributeOnDeserializingMethod is { ClientRequiresNamedArguments: true }; - return (IProgress?)this.formatter.FormatterProgressTracker.CreateProgress(this.formatter.JsonRpc, token, typeToConvert, clientRequiresNamedArgs); + return (IProgress)this.formatter.FormatterProgressTracker.CreateProgress(this.formatter.JsonRpc, token, typeToConvert, clientRequiresNamedArgs); } - public override void Write(Utf8JsonWriter writer, IProgress? value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, IProgress value, JsonSerializerOptions options) { - long progressId = this.formatter.FormatterProgressTracker.GetTokenForProgress(value!); - writer.WriteNumberValue(progressId); + writer.WriteNumberValue(this.formatter.FormatterProgressTracker.GetTokenForProgress(value)); } } } @@ -834,7 +826,7 @@ internal AsyncEnumerableConverter(SystemTextJsonFormatter formatter) return (JsonConverter)Activator.CreateInstance(converterType, this.formatter)!; } - private class Converter : JsonConverter?> + private class Converter : JsonConverter> { private readonly SystemTextJsonFormatter formatter; @@ -843,13 +835,8 @@ public Converter(SystemTextJsonFormatter formatter) this.formatter = formatter; } - public override IAsyncEnumerable? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override IAsyncEnumerable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - using JsonDocument wrapper = JsonDocument.ParseValue(ref reader); JsonElement? handle = null; if (wrapper.RootElement.TryGetProperty(MessageFormatterEnumerableTracker.TokenPropertyName, out JsonElement enumToken)) @@ -867,14 +854,8 @@ public Converter(SystemTextJsonFormatter formatter) return this.formatter.EnumerableTracker.CreateEnumerableProxy(handle, prefetchedItems); } - public override void Write(Utf8JsonWriter writer, IAsyncEnumerable? value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, IAsyncEnumerable value, JsonSerializerOptions options) { - if (value is null) - { - writer.WriteNullValue(); - return; - } - (IReadOnlyList Elements, bool Finished) prefetched = value.TearOffPrefetchedElements(); long token = this.formatter.EnumerableTracker.GetToken(value); writer.WriteStartObject(); @@ -918,7 +899,7 @@ public override bool CanConvert(Type typeToConvert) targetOptions)!; } - private class Converter : JsonConverter + private class Converter : JsonConverter where T : class { private readonly SystemTextJsonFormatter formatter; @@ -932,32 +913,21 @@ public Converter(SystemTextJsonFormatter formatter, JsonRpcProxyOptions proxyOpt this.targetOptions = targetOptions; } - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - MessageFormatterRpcMarshaledContextTracker.MarshalToken token = JsonSerializer.Deserialize(ref reader, options); - return (T?)this.formatter.RpcMarshaledContextTracker.GetObject(typeof(T), token, this.proxyOptions); + return (T)this.formatter.RpcMarshaledContextTracker.GetObject(typeof(T), token, this.proxyOptions); } - public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { - if (value is null) - { - writer.WriteNullValue(); - return; - } - MessageFormatterRpcMarshaledContextTracker.MarshalToken token = this.formatter.RpcMarshaledContextTracker.GetToken(value, this.targetOptions, typeof(T)); JsonSerializer.Serialize(writer, token, options); } } } - private class DuplexPipeConverter : JsonConverter + private class DuplexPipeConverter : JsonConverter { private readonly SystemTextJsonFormatter formatter; @@ -968,27 +938,18 @@ internal DuplexPipeConverter(SystemTextJsonFormatter formatter) public override bool CanConvert(Type typeToConvert) => typeof(IDuplexPipe).IsAssignableFrom(typeToConvert); - public override IDuplexPipe? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override IDuplexPipe Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return reader.TokenType == JsonTokenType.Null - ? null - : this.formatter.DuplexPipeTracker!.GetPipe(reader.GetUInt64()); + return this.formatter.DuplexPipeTracker.GetPipe(reader.GetUInt64()); } - public override void Write(Utf8JsonWriter writer, IDuplexPipe? value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, IDuplexPipe value, JsonSerializerOptions options) { - if (this.formatter.DuplexPipeTracker.GetULongToken(value) is ulong token) - { - writer.WriteNumberValue(token); - } - else - { - writer.WriteNullValue(); - } + writer.WriteNumberValue(this.formatter.DuplexPipeTracker.GetULongToken(value).Value); } } - private class PipeReaderConverter : JsonConverter + private class PipeReaderConverter : JsonConverter { private readonly SystemTextJsonFormatter formatter; @@ -999,27 +960,18 @@ internal PipeReaderConverter(SystemTextJsonFormatter formatter) public override bool CanConvert(Type typeToConvert) => typeof(PipeReader).IsAssignableFrom(typeToConvert); - public override PipeReader? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PipeReader Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return reader.TokenType == JsonTokenType.Null - ? null - : this.formatter.DuplexPipeTracker!.GetPipeReader(reader.GetUInt64()); + return this.formatter.DuplexPipeTracker!.GetPipeReader(reader.GetUInt64()); } - public override void Write(Utf8JsonWriter writer, PipeReader? value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PipeReader value, JsonSerializerOptions options) { - if (this.formatter.DuplexPipeTracker.GetULongToken(value) is ulong token) - { - writer.WriteNumberValue(token); - } - else - { - writer.WriteNullValue(); - } + writer.WriteNumberValue(this.formatter.DuplexPipeTracker.GetULongToken(value).Value); } } - private class PipeWriterConverter : JsonConverter + private class PipeWriterConverter : JsonConverter { private readonly SystemTextJsonFormatter formatter; @@ -1030,27 +982,18 @@ internal PipeWriterConverter(SystemTextJsonFormatter formatter) public override bool CanConvert(Type typeToConvert) => typeof(PipeWriter).IsAssignableFrom(typeToConvert); - public override PipeWriter? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PipeWriter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return reader.TokenType == JsonTokenType.Null - ? null - : this.formatter.DuplexPipeTracker!.GetPipeWriter(reader.GetUInt64()); + return this.formatter.DuplexPipeTracker.GetPipeWriter(reader.GetUInt64()); } - public override void Write(Utf8JsonWriter writer, PipeWriter? value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PipeWriter value, JsonSerializerOptions options) { - if (this.formatter.DuplexPipeTracker.GetULongToken(value) is ulong token) - { - writer.WriteNumberValue(token); - } - else - { - writer.WriteNullValue(); - } + writer.WriteNumberValue(this.formatter.DuplexPipeTracker.GetULongToken(value).Value); } } - private class StreamConverter : JsonConverter + private class StreamConverter : JsonConverter { private readonly SystemTextJsonFormatter formatter; @@ -1061,27 +1004,18 @@ internal StreamConverter(SystemTextJsonFormatter formatter) public override bool CanConvert(Type typeToConvert) => typeof(Stream).IsAssignableFrom(typeToConvert); - public override Stream? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override Stream Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return reader.TokenType == JsonTokenType.Null - ? null - : this.formatter.DuplexPipeTracker!.GetPipe(reader.GetUInt64()).AsStream(); + return this.formatter.DuplexPipeTracker.GetPipe(reader.GetUInt64()).AsStream(); } - public override void Write(Utf8JsonWriter writer, Stream? value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, Stream value, JsonSerializerOptions options) { - if (this.formatter.DuplexPipeTracker.GetULongToken(value?.UsePipe()) is ulong token) - { - writer.WriteNumberValue(token); - } - else - { - writer.WriteNullValue(); - } + writer.WriteNumberValue(this.formatter.DuplexPipeTracker.GetULongToken(value.UsePipe()).Value); } } - private class ExceptionConverter : JsonConverter + private class ExceptionConverter : JsonConverter { /// /// Tracks recursion count while serializing or deserializing an exception. @@ -1100,10 +1034,6 @@ internal ExceptionConverter(SystemTextJsonFormatter formatter) public override Exception? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { Assumes.NotNull(this.formatter.JsonRpc); - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } exceptionRecursionCounter.Value++; try @@ -1136,14 +1066,8 @@ internal ExceptionConverter(SystemTextJsonFormatter formatter) } } - public override void Write(Utf8JsonWriter writer, Exception? value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) { - if (value is null) - { - writer.WriteNullValue(); - return; - } - // We have to guard our own recursion because the serializer has no visibility into inner exceptions. // Each exception in the russian doll is a new serialization job from its perspective. exceptionRecursionCounter.Value++; diff --git a/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs b/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs index 4250820d..96bb6ff2 100644 --- a/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs +++ b/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs @@ -1,6 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using Nerdbank.Streams; +using Newtonsoft.Json.Linq; using StreamJsonRpc; using Xunit.Abstractions; @@ -11,5 +16,54 @@ public SystemTextJsonFormatterTests(ITestOutputHelper logger) { } + protected new SystemTextJsonFormatter Formatter => (SystemTextJsonFormatter)base.Formatter; + + [Fact] + public void DataContractAttributesWinOverSTJAttributes() + { + IJsonRpcMessageFactory messageFactory = this.Formatter; + JsonRpcRequest requestMessage = messageFactory.CreateRequestMessage(); + requestMessage.Method = "test"; + requestMessage.Arguments = new[] { new DCSClass { C = 1 } }; + + using Sequence sequence = new(); + this.Formatter.Serialize(sequence, requestMessage); + + using JsonDocument doc = JsonDocument.Parse(sequence); + this.Logger.WriteLine(doc.RootElement.ToString()); + Assert.Equal(1, doc.RootElement.GetProperty("params")[0].GetProperty("A").GetInt32()); + } + + [Fact] + public void STJAttributesWinOverDataMemberWithoutDataContract() + { + IJsonRpcMessageFactory messageFactory = this.Formatter; + JsonRpcRequest requestMessage = messageFactory.CreateRequestMessage(); + requestMessage.Method = "test"; + requestMessage.Arguments = new[] { new STJClass { C = 1 } }; + + using Sequence sequence = new(); + this.Formatter.Serialize(sequence, requestMessage); + + using JsonDocument doc = JsonDocument.Parse(sequence); + this.Logger.WriteLine(doc.RootElement.ToString()); + Assert.Equal(1, doc.RootElement.GetProperty("params")[0].GetProperty("B").GetInt32()); + } + protected override SystemTextJsonFormatter CreateFormatter() => new(); + + [DataContract] + public class DCSClass + { + [DataMember(Name = "A")] + [JsonPropertyName("B")] + public int C { get; set; } + } + + public class STJClass + { + [DataMember(Name = "A")] + [JsonPropertyName("B")] + public int C { get; set; } + } }