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)
+ {
+ }
+}