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/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/doc/extensibility.md b/doc/extensibility.md index 3176b9d5..21005758 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,24 @@ 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. + +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/ +[SystemTextJson]: https://learn.microsoft.com/dotnet/standard/serialization/system-text-json/overview [spec]: https://www.jsonrpc.org/specification diff --git a/src/StreamJsonRpc/FormatterBase.cs b/src/StreamJsonRpc/FormatterBase.cs new file mode 100644 index 00000000..ddc6498b --- /dev/null +++ b/src/StreamJsonRpc/FormatterBase.cs @@ -0,0 +1,506 @@ +// 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.Diagnostics.CodeAnalysis; +using System.IO.Pipelines; +using System.Reflection; +using System.Runtime.Serialization; +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() + { + } + + /// + /// 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; } + } + + /// + 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 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. + /// + 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. + /// + /// A value to dispose of when deserialization has completed. + /// + protected DeserializationTracking TrackDeserialization(JsonRpcMessage message, ReadOnlySpan parameters = default) => new(this, message, parameters); + + /// + /// 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); + + 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. + /// + public struct DeserializationTracking : IDisposable + { + private readonly FormatterBase? formatter; + + /// + /// Initializes a new instance of the struct. + /// + /// The formatter. + /// The message being deserialized. + /// 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. + // 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; + + // 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; + } + } + + /// + /// Clears deserialization state. + /// + public void Dispose() + { + if (this.formatter is not null) + { + this.formatter.DeserializingMessageWithId = default; + this.formatter.DeserializingMessage = null; + this.formatter.ApplicableMethodAttributeOnDeserializingMethod = 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; + } + } + + /// + /// 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; + } + + /// + /// Gets a dictionary of top-level properties that should be serialized. + /// + /// Thrown if called on an inbound message. + 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); + } + + /// + /// 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; } + + 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; + } + + /// + /// 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; } + + 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() + { + } + } + + /// + /// 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; } + + 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 e7eabb20..c687cb31 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. /// @@ -204,11 +150,6 @@ public JsonMessageFormatter(Encoding encoding) }; } - private interface IMessageWithTopLevelPropertyBag - { - TopLevelPropertyBag? TopLevelPropertyBag { get; set; } - } - /// /// Gets or sets the encoding to use for transmitted messages. /// @@ -248,90 +189,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 - { - 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 + /// + public new MultiplexingStream? MultiplexingStream { - 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 +207,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 +225,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 +240,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); @@ -461,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; @@ -497,22 +353,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 +473,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 +518,6 @@ private void TokenizeUserData(JsonRpcMessage jsonRpcMessage) break; } } - finally - { - this.serializingMessageWithId = default; - this.serializingRequest = false; - } } /// @@ -752,39 +594,7 @@ 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"); - - if (this.formatterProgressTracker 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.rpc?.TraceSource.TraceData(TraceEventType.Error, (int)JsonRpc.TraceEvents.ProgressNotificationError, e); - } - } - - return new JsonRpcRequest(this) + JsonRpcRequest request = new(this) { RequestId = id, Method = json.Value("method"), @@ -793,6 +603,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) @@ -853,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 @@ -869,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; } /// @@ -880,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.Value is null ? JValue.CreateNull() : JToken.FromObject(property.Value.Value, 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; @@ -898,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; @@ -916,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; @@ -942,17 +764,9 @@ internal JsonRpcRequest(JsonMessageFormatter formatter) this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); } - [JsonIgnore] - [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.rpc?.GetJsonRpcMethodAttribute(this.Method, parameters) : null; - try + using (this.formatter.TrackDeserialization(this, parameters)) { if (parameters.Length == 1 && this.NamedArguments is not null) { @@ -970,42 +784,22 @@ 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)); - } - - // Deserialization of messages should never occur concurrently for a single instance of a formatter. - Assumes.True(this.formatter.deserializingMessageWithId.IsEmpty); - this.formatter.deserializingMessageWithId = this.RequestId; - this.formatter.deserializingMessage = this; - try - { - typedArguments[0] = obj.ToObject(parameters[0].ParameterType, this.formatter.JsonSerializer); - } - finally - { - this.formatter.deserializingMessageWithId = default; - this.formatter.deserializingMessage = null; - } - - return ArgumentMatchResult.Success; + obj.Add(new JProperty(property.Key, property.Value)); } + + typedArguments[0] = obj.ToObject(parameters[0].ParameterType, this.formatter.JsonSerializer); + + return ArgumentMatchResult.Success; } } return base.TryGetTypedArguments(parameters, typedArguments); } - finally - { - // Clear this, because we might choose another overload with a different attribute, and we don't want to 'leak' an attribute that isn't on the overload that is ultimately picked. - this.ApplicableMethodAttribute = null; - } } public override bool TryGetArgumentByNameOrIndex(string? name, int position, Type? typeHint, out object? value) @@ -1015,27 +809,18 @@ public override bool TryGetArgumentByNameOrIndex(string? name, int position, Typ var token = (JToken?)value; try { - // Deserialization of messages should never occur concurrently for a single instance of a formatter. - Assumes.True(this.formatter.deserializingMessageWithId.IsEmpty); - this.formatter.deserializingMessageWithId = this.RequestId; - this.formatter.deserializingMessage = this; - try + using (this.formatter.TrackDeserialization(this)) { value = token?.ToObject(typeHint!, this.formatter.JsonSerializer); // Null typeHint is allowed (https://github.com/JamesNK/Newtonsoft.Json/pull/2562) } - finally - { - this.formatter.deserializingMessageWithId = default; - this.formatter.deserializingMessage = null; - } return true; } catch (Exception ex) { - if (this.formatter.rpc?.TraceSource.Switch.ShouldTrace(TraceEventType.Warning) ?? false) + if (this.formatter.JsonRpc?.TraceSource.Switch.ShouldTrace(TraceEventType.Warning) ?? false) { - this.formatter.rpc.TraceSource.TraceEvent(TraceEventType.Warning, (int)JsonRpc.TraceEvents.MethodArgumentDeserializationFailure, Resources.FailureDeserializingRpcArgument, name, position, typeHint, ex); + this.formatter.JsonRpc.TraceSource.TraceEvent(TraceEventType.Warning, (int)JsonRpc.TraceEvents.MethodArgumentDeserializationFailure, Resources.FailureDeserializingRpcArgument, name, position, typeHint, ex); } throw new RpcArgumentDeserializationException(name, position, typeHint, ex); @@ -1045,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 is not null && this.TopLevelPropertyBag.TryGetTopLevelProperty(name, out value); - } - - 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; @@ -1076,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."); @@ -1096,31 +860,14 @@ 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); } } - 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 is not null && this.TopLevelPropertyBag.TryGetTopLevelProperty(name, out value); - } - - 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; @@ -1129,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 is not null && this.TopLevelPropertyBag.TryGetTopLevelProperty(name, out value); - } - - 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] @@ -1224,7 +950,7 @@ public JsonProgressClientConverter(JsonMessageFormatter formatter) this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); } - public override bool CanConvert(Type objectType) => MessageFormatterProgressTracker.IsSupportedProgressType(objectType); + public override bool CanConvert(Type objectType) => MessageFormatterProgressTracker.CanSerialize(objectType); public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { @@ -1250,10 +976,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) { @@ -1262,10 +985,10 @@ public override bool CanConvert(Type objectType) return null; } - Assumes.NotNull(this.formatter.rpc); + Assumes.NotNull(this.formatter.JsonRpc); JToken token = JToken.Load(reader); - bool clientRequiresNamedArgs = this.formatter.deserializingMessage is JsonRpcRequest { ApplicableMethodAttribute: { ClientRequiresNamedArguments: true } }; - return this.formatter.FormatterProgressTracker.CreateProgress(this.formatter.rpc, token, objectType, clientRequiresNamedArgs); + bool clientRequiresNamedArgs = this.formatter.ApplicableMethodAttributeOnDeserializingMethod?.ClientRequiresNamedArguments is true; + return this.formatter.FormatterProgressTracker.CreateProgress(this.formatter.JsonRpc, token, objectType, clientRequiresNamedArgs); } public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) @@ -1407,12 +1130,12 @@ public DuplexPipeConverter(JsonMessageFormatter jsonMessageFormatter) public override IDuplexPipe? ReadJson(JsonReader reader, Type objectType, IDuplexPipe? existingValue, bool hasExistingValue, JsonSerializer serializer) { ulong? tokenId = JToken.Load(reader).Value(); - 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 +1317,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 +1331,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 +1364,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 +1385,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..8b43c0eb 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. /// @@ -197,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 . @@ -213,93 +158,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 +183,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 +237,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 +249,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. /// @@ -860,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 @@ -966,11 +813,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); } @@ -1035,10 +882,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.ApplicableMethodAttributeOnDeserializingMethod?.ClientRequiresNamedArguments is 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 +1395,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 +1406,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 +1435,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 +1454,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 +1522,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 +1539,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; - } } } @@ -1799,30 +1639,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.rpc?.TraceSource.TraceData(TraceEventType.Error, (int)JsonRpc.TraceEvents.ProgressNotificationError, ex); - } - } + this.formatter.TryHandleSpecialIncomingMessage(result); reader.Depth--; return result; @@ -1830,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) @@ -1842,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); @@ -1906,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) @@ -2029,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); @@ -2047,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); } } @@ -2103,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); @@ -2117,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); } } @@ -2257,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 @@ -2271,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; @@ -2282,40 +2098,44 @@ 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.inboundUnknownProperties?.Count ?? this.OutboundProperties?.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.inboundUnknownProperties is not null) { - throw new InvalidOperationException(Resources.OutboundMessageOnly); - } - - Verify.Operation(!this.outboundPropertiesAlreadyWritten, Resources.UsableOnceOnly); + // 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.outboundUnknownProperties) + ////foreach (KeyValuePair> entry in this.inboundUnknownProperties) + ////{ + //// writer.Write(entry.Key); + //// writer.Write(entry.Value); + ////} + } + else { - writer.Write(entry.Key); - writer.WriteRaw(entry.Value); - entry.Value.Reset(); + foreach (KeyValuePair entry in this.OutboundProperties) + { + writer.Write(entry.Key); + MessagePackSerializer.Serialize(entry.Value.DeclaredType, ref writer, entry.Value.Value, 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) { @@ -2333,25 +2153,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; @@ -2360,22 +2166,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; @@ -2390,47 +2186,23 @@ 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; } - internal JsonRpcMethodAttribute? ApplicableMethodAttribute { get; private 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) { - // 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; - 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 { - // Deserialization of messages should never occur concurrently for a single instance of a formatter. - Assumes.True(this.formatter.deserializingMessageWithId.IsEmpty); - this.formatter.deserializingMessageWithId = this.RequestId; - this.formatter.deserializingMessage = this; typedArguments[0] = MessagePackSerializer.Deserialize(parameters[0].ParameterType, ref reader, this.formatter.userDataSerializationOptions); return ArgumentMatchResult.Success; } @@ -2438,21 +2210,11 @@ public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan(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 is not null && this.TopLevelPropertyBag.TryGetTopLevelProperty(name, out value); + 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; @@ -2537,17 +2286,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) @@ -2560,25 +2300,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 is not null && this.TopLevelPropertyBag.TryGetTopLevelProperty(name, out value); - } - - 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."); @@ -2595,11 +2316,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; @@ -2610,11 +2340,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; @@ -2623,25 +2353,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 is not null && this.TopLevelPropertyBag.TryGetTopLevelProperty(name, out value); - } - - 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/Reflection/MessageFormatterProgressTracker.cs b/src/StreamJsonRpc/Reflection/MessageFormatterProgressTracker.cs index f3af5b93..fbfced72 100644 --- a/src/StreamJsonRpc/Reflection/MessageFormatterProgressTracker.cs +++ b/src/StreamJsonRpc/Reflection/MessageFormatterProgressTracker.cs @@ -77,7 +77,22 @@ public MessageFormatterProgressTracker(JsonRpc jsonRpc, IJsonRpcFormatterState f /// /// 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/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/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/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..049396c0 --- /dev/null +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -0,0 +1,1338 @@ +// 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.Collections.Concurrent; +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; +using System.Text; +using System.Text.Json; +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; + +namespace StreamJsonRpc; + +/// +/// A formatter that emits UTF-8 encoded JSON where user data should be serializable via the . +/// +public class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcFormatterState, IJsonRpcMessageFactory, IJsonRpcFormatterTracingCallbacks +{ + private static readonly JsonWriterOptions WriterOptions = 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 readonly ToStringHelper serializationToStringHelper = new ToStringHelper(); + + 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. + /// + public SystemTextJsonFormatter() + { + 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 = new DataContractResolver(onlyRecognizeDecoratedTypes: true), + }); + } + + /// + public Encoding Encoding + { + get => DefaultEncoding; + set => throw new NotSupportedException(); + } + + /// + /// 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)); + } + + /// + public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer) => this.Deserialize(contentBuffer, this.Encoding); + + /// + public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding encoding) + { + if (encoding is not UTF8Encoding) + { + throw new NotSupportedException("Only our default encoding is supported."); + } + + 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."); + } + + JsonRpcMessage message; + if (document.RootElement.TryGetProperty(Utf8Strings.method, out JsonElement methodElement)) + { + JsonRpcRequest request = new(this) + { + 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; + } + else if (document.RootElement.TryGetProperty(Utf8Strings.result, out JsonElement resultElement)) + { + JsonRpcResult result = new(this) + { + RequestId = ReadRequestId(), + JsonResult = resultElement, + }; + message = result; + } + else if (document.RootElement.TryGetProperty(Utf8Strings.error, out JsonElement errorElement)) + { + JsonRpcError error = new(this) + { + RequestId = ReadRequestId(), + 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, + }, + }; + + message = error; + } + else + { + throw new JsonException("Expected a request, result, or error message."); + } + + if (document.RootElement.TryGetProperty(Utf8Strings.jsonrpc, out JsonElement jsonRpcElement)) + { + 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. + message.Version = "1.0"; + } + + if (message is IMessageWithTopLevelPropertyBag messageWithTopLevelPropertyBag) + { + messageWithTopLevelPropertyBag.TopLevelPropertyBag = new TopLevelPropertyBag(document, this.massagedUserDataSerializerOptions); + } + + RequestId ReadRequestId() + { + return document.RootElement.TryGetProperty(Utf8Strings.id, out JsonElement idElement) + ? idElement.Deserialize(BuiltInSerializerOptions) + : RequestId.NotSpecified; + } + + IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc; + tracingCallbacks?.OnMessageDeserialized(message, document.RootElement); + + this.TryHandleSpecialIncomingMessage(message); + + return message; + } + + /// + public object GetJsonText(JsonRpcMessage message) => throw new NotSupportedException(); + + /// + public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message) + { + using (this.TrackSerialization(message)) + { + try + { + 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); + 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); + 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() + { + 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 id) + { + if (!id.IsEmpty) + { + writer.WritePropertyName(Utf8Strings.id); + RequestIdJsonConverter.Instance.Write(writer, id, BuiltInSerializerOptions); + } + } + + void WriteArguments(Protocol.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], request.ArgumentListDeclaredTypes?[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, request.NamedArgumentDeclaredTypes?[argument.Key]); + } + + 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, declaredType: null); + } + } + + void WriteResult(Protocol.JsonRpcResult result) + { + writer.WritePropertyName(Utf8Strings.result); + WriteUserData(result.Result, result.ResultDeclaredType); + } + + void WriteError(Protocol.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, null); + } + + writer.WriteEndObject(); + } + + void WriteUserData(object? value, Type? declaredType) + { + if (declaredType is not null && value is not null) + { + JsonSerializer.Serialize(writer, value, declaredType, this.massagedUserDataSerializerOptions); + } + else + { + JsonSerializer.Serialize(writer, value, this.massagedUserDataSerializerOptions); + } + } + } + catch (Exception ex) + { + throw new JsonException(Resources.SerializationFailure, ex); + } + } + } + + 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); + + Protocol.JsonRpcResult IJsonRpcMessageFactory.CreateResultMessage() => new JsonRpcResult(this); + + 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)); + 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)); + options.Converters.Add(new StreamConverter(this)); + + // Add support for serializing exceptions. + options.Converters.Add(new ExceptionConverter(this)); + + return options; + } + + 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 traceparent => "traceparent"u8; + + internal static ReadOnlySpan tracestate => "tracestate"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 + } + + private class TopLevelPropertyBag : TopLevelPropertyBagBase + { + private readonly JsonDocument? incomingMessage; + private readonly JsonSerializerOptions jsonSerializerOptions; + + /// + /// 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) + : base(isOutbound: false) + { + 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) + : base(isOutbound: true) + { + this.jsonSerializerOptions = jsonSerializerOptions; + } + + internal void WriteProperties(Utf8JsonWriter writer) + { + if (this.incomingMessage 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. + // 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); + } + } + } + + protected internal override 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; + } + } + + private class JsonRpcRequest : JsonRpcRequestBase + { + private readonly SystemTextJsonFormatter formatter; + + private int? argumentCount; + + private JsonElement? jsonArguments; + + internal JsonRpcRequest(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override int ArgumentCount => this.argumentCount ?? base.ArgumentCount; + + internal JsonElement? JsonArguments + { + get => this.jsonArguments; + init + { + this.jsonArguments = value; + if (value.HasValue) + { + this.argumentCount = CountArguments(value.Value); + } + } + } + + 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) + { + value = null; + return false; + } + + 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 JsonException("Unexpected value kind for arguments: " + (this.JsonArguments?.ValueKind.ToString() ?? "null")); + } + + try + { + using (this.formatter.TrackDeserialization(this)) + { + 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) + { + throw new RpcArgumentDeserializationException(name, position, typeHint, ex); + } + + return valueElement.HasValue; + } + + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); + + protected override void ReleaseBuffers() + { + base.ReleaseBuffers(); + this.jsonArguments = null; + this.formatter.deserializingDocument?.Dispose(); + this.formatter.deserializingDocument = null; + } + + private static int CountArguments(JsonElement arguments) + { + int count; + switch (arguments.ValueKind) + { + case JsonValueKind.Array: + count = arguments.GetArrayLength(); + + break; + case JsonValueKind.Object: + count = 0; + foreach (JsonProperty property in arguments.EnumerateObject()) + { + count++; + } + + break; + default: + throw new InvalidOperationException("Unexpected value kind: " + arguments.ValueKind); + } + + return count; + } + } + + private class JsonRpcResult : JsonRpcResultBase + { + private readonly SystemTextJsonFormatter formatter; + + private Exception? resultDeserializationException; + + internal JsonRpcResult(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + + internal JsonElement? JsonResult { get; set; } + + 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(this.formatter.massagedUserDataSerializerOptions)!; + } + + 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.formatter.massagedUserDataSerializerOptions); + 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 = new JsonException(string.Format(CultureInfo.CurrentCulture, Resources.FailureDeserializingRpcResult, resultType.Name, ex.GetType().Name, ex.Message), ex); + } + } + + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); + + protected override void ReleaseBuffers() + { + base.ReleaseBuffers(); + this.JsonResult = null; + this.formatter.deserializingDocument?.Dispose(); + this.formatter.deserializingDocument = null; + } + } + + private class JsonRpcError : JsonRpcErrorBase + { + private readonly SystemTextJsonFormatter formatter; + + public JsonRpcError(SystemTextJsonFormatter formatter) + { + this.formatter = formatter; + } + + internal new ErrorDetail? Error + { + get => (ErrorDetail?)base.Error; + set => base.Error = value; + } + + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); + + protected override void ReleaseBuffers() + { + base.ReleaseBuffers(); + if (this.Error is { } detail) + { + detail.JsonData = null; + } + + this.formatter.deserializingDocument?.Dispose(); + this.formatter.deserializingDocument = 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.massagedUserDataSerializerOptions); + } + catch (JsonException) + { + // Deserialization failed. Try returning array/dictionary based primitive objects. + try + { + return this.JsonData.Value.Deserialize(this.formatter.massagedUserDataSerializerOptions); + } + catch (JsonException) + { + return null; + } + } + } + + protected internal override void SetExpectedDataType(Type dataType) + { + 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; + } + } + } + + 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(); + } + } + } + + private class ProgressConverterFactory : JsonConverterFactory + { + private readonly SystemTextJsonFormatter formatter; + + internal ProgressConverterFactory(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 IProgress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + 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.ApplicableMethodAttributeOnDeserializingMethod is { ClientRequiresNamedArguments: true }; + return (IProgress)this.formatter.FormatterProgressTracker.CreateProgress(this.formatter.JsonRpc, token, typeToConvert, clientRequiresNamedArgs); + } + + public override void Write(Utf8JsonWriter writer, IProgress value, JsonSerializerOptions options) + { + writer.WriteNumberValue(this.formatter.FormatterProgressTracker.GetTokenForProgress(value)); + } + } + } + + 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) + { + 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) + { + (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 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) + { + 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) + { + 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; + + 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 this.formatter.DuplexPipeTracker.GetPipe(reader.GetUInt64()); + } + + public override void Write(Utf8JsonWriter writer, IDuplexPipe value, JsonSerializerOptions options) + { + writer.WriteNumberValue(this.formatter.DuplexPipeTracker.GetULongToken(value).Value); + } + } + + 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 this.formatter.DuplexPipeTracker!.GetPipeReader(reader.GetUInt64()); + } + + public override void Write(Utf8JsonWriter writer, PipeReader value, JsonSerializerOptions options) + { + writer.WriteNumberValue(this.formatter.DuplexPipeTracker.GetULongToken(value).Value); + } + } + + 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 this.formatter.DuplexPipeTracker.GetPipeWriter(reader.GetUInt64()); + } + + public override void Write(Utf8JsonWriter writer, PipeWriter value, JsonSerializerOptions options) + { + writer.WriteNumberValue(this.formatter.DuplexPipeTracker.GetULongToken(value).Value); + } + } + + 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 this.formatter.DuplexPipeTracker.GetPipe(reader.GetUInt64()).AsStream(); + } + + public override void Write(Utf8JsonWriter writer, Stream value, JsonSerializerOptions options) + { + writer.WriteNumberValue(this.formatter.DuplexPipeTracker.GetULongToken(value.UsePipe()).Value); + } + } + + private class ExceptionConverter : JsonConverter + { + /// + /// Tracks recursion count while serializing or deserializing an exception. + /// + private static ThreadLocal exceptionRecursionCounter = new(); + + private readonly SystemTextJsonFormatter formatter; + + internal 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); + + 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) + { + // 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 ConcurrentDictionary typeInfoCache = new(); + + private readonly bool onlyRecognizeDecoratedTypes; + + private readonly DefaultJsonTypeInfoResolver fallbackResolver = new(); + + internal 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.TryAdd(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; + } + } + } + + /// + 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; + } + } +} diff --git a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Shipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Shipped.txt index 7b9d6004..b5cf3455 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Shipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/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.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt index e69de29b..423af780 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,67 @@ +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 +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, System.ReadOnlySpan parameters) -> 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.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 +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.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 +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.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 +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.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 e69de29b..423af780 100644 --- a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt @@ -0,0 +1,67 @@ +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 +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, System.ReadOnlySpan parameters) -> 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.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 +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.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 +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.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 +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/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/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 new file mode 100644 index 00000000..86eeb1e6 --- /dev/null +++ b/test/StreamJsonRpc.Tests/DisposableProxySystemTextJsonTests.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. + +using System.Text.Json; + +public class DisposableProxySystemTextJsonTests : DisposableProxyTests +{ + public DisposableProxySystemTextJsonTests(ITestOutputHelper logger) + : base(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/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/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/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] 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/JsonRpcJsonHeadersTests.cs b/test/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs index d028e3cf..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() { @@ -51,6 +47,25 @@ public async Task CanInvokeServerMethodWithParameterPassedAsObject() 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() { @@ -87,33 +102,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 +114,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 +125,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 +156,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..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,8 +29,10 @@ internal interface IMessagePackServer Task IsExtensionArgNonNull(CustomExtensionType extensionValue); } + protected override Type FormatterExceptionType => typeof(MessagePackSerializationException); + [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 +53,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/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/JsonRpcSystemTextJsonHeadersTests.cs b/test/StreamJsonRpc.Tests/JsonRpcSystemTextJsonHeadersTests.cs new file mode 100644 index 00000000..6071d9f1 --- /dev/null +++ b/test/StreamJsonRpc.Tests/JsonRpcSystemTextJsonHeadersTests.cs @@ -0,0 +1,89 @@ +// 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; +using System.Text.Json.Serialization; +using Microsoft.VisualStudio.Threading; + +public class JsonRpcSystemTextJsonHeadersTests : JsonRpcTests +{ + public JsonRpcSystemTextJsonHeadersTests(ITestOutputHelper logger) + : base(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); + 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 TypeThrowsWhenDeserializedConverter : JsonConverter + { + public override TypeThrowsWhenDeserialized? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw CreateExceptionToBeThrownByDeserializer(); + } + + public override void Write(Utf8JsonWriter writer, TypeThrowsWhenDeserialized value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + } +} diff --git a/test/StreamJsonRpc.Tests/JsonRpcTests.cs b/test/StreamJsonRpc.Tests/JsonRpcTests.cs index 4cb36bc2..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] @@ -2907,6 +2905,45 @@ public void JoinableTaskFactory_IntegrationClientSideOnly() }); } + [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 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 +3699,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; } 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 new file mode 100644 index 00000000..4a57942b --- /dev/null +++ b/test/StreamJsonRpc.Tests/MarshalableProxySystemTextJsonTests.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. + +using System.Text.Json; + +public class MarshalableProxySystemTextJsonTests : MarshalableProxyTests +{ + public MarshalableProxySystemTextJsonTests(ITestOutputHelper logger) + : base(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); 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/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 cbf69b39..f77c8ccd 100644 --- a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj +++ b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj @@ -8,20 +8,34 @@ + + + + + + + + + + + + + + diff --git a/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs b/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs new file mode 100644 index 00000000..96bb6ff2 --- /dev/null +++ b/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs @@ -0,0 +1,69 @@ +// 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; + +public class SystemTextJsonFormatterTests : FormatterTestBase +{ + public SystemTextJsonFormatterTests(ITestOutputHelper logger) + : base(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; } + } +} diff --git a/test/StreamJsonRpc.Tests/TargetObjectEventsSystemTextJsonTests.cs b/test/StreamJsonRpc.Tests/TargetObjectEventsSystemTextJsonTests.cs new file mode 100644 index 00000000..e98fde4f --- /dev/null +++ b/test/StreamJsonRpc.Tests/TargetObjectEventsSystemTextJsonTests.cs @@ -0,0 +1,77 @@ +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; + } + + 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.")); + } + + 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 new file mode 100644 index 00000000..b6aeb0a2 --- /dev/null +++ b/test/StreamJsonRpc.Tests/Usings.cs @@ -0,0 +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. + +global using Microsoft; +global using StreamJsonRpc; +global using StreamJsonRpc.Protocol; +global using Xunit; +global using Xunit.Abstractions; 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) + { + } +}